Skip to content

Commit

Permalink
Merge remote-tracking branch 'cve/security/3.2.3/21971-remote-executi…
Browse files Browse the repository at this point in the history
…on-through-resource_type' into release_3.2.4

* cve/security/3.2.3/21971-remote-execution-through-resource_type:
  (#21971) Fixes PathPattern's usage of Dir.glob for Windows
  (#21971) Fix TypeLoader#import_all on Ruby 1.8.7
  (#21971) Create system for safely dealing with path patterns
  (#21971) Split import and autoloading code paths
  (#21971) Add test for accessing based on master being at root
  (#21971) Check for possible directory traversal
  (Maint) Clean up specs
  (Maint) Use dirname instead of regexes
  (#21971) Create test to show exploit of resource_type

Conflicts:
	lib/puppet/parser/type_loader.rb
	spec/unit/parser/type_loader_spec.rb
  • Loading branch information
zaphod42 authored and jpartlow committed Aug 7, 2013
2 parents a23fd4e + a0f8a32 commit a177c9d
Show file tree
Hide file tree
Showing 13 changed files with 414 additions and 226 deletions.
2 changes: 2 additions & 0 deletions lib/puppet.rb
Expand Up @@ -27,6 +27,8 @@
#
# @api public
module Puppet
require 'puppet/file_system'

class << self
include Puppet::Util
attr_reader :features
Expand Down
3 changes: 3 additions & 0 deletions lib/puppet/file_system.rb
@@ -0,0 +1,3 @@
module Puppet::FileSystem
require 'puppet/file_system/path_pattern'
end
98 changes: 98 additions & 0 deletions lib/puppet/file_system/path_pattern.rb
@@ -0,0 +1,98 @@
require 'pathname'

module Puppet::FileSystem
class PathPattern
class InvalidPattern < Puppet::Error; end

TRAVERSAL = /\.\./
ABSOLUTE_UNIX = /^\//
ABSOLUTE_WINDOWS = /^[a-z]:/i
#ABSOLUT_VODKA #notappearinginthisclass
CURRENT_DRIVE_RELATIVE_WINDOWS = /^\\/

def self.relative(pattern)
RelativePathPattern.new(pattern)
end

def self.absolute(pattern)
AbsolutePathPattern.new(pattern)
end

class << self
protected :new
end

# @param prefix [AbsolutePathPattern] An absolute path pattern instance
# @return [AbsolutePathPattern] A new AbsolutePathPattern prepended with
# the passed prefix's pattern.
def prefix_with(prefix)
new_pathname = prefix.pathname + pathname
self.class.absolute(new_pathname.to_s)
end

def glob
Dir.glob(pathname.to_s)
end

def to_s
pathname.to_s
end

protected

attr_reader :pathname

private

def validate(pattern)
stripped = pattern.strip
case stripped
when TRAVERSAL
raise(InvalidPattern, "PathPatterns cannot be created with directory traversals.")
when CURRENT_DRIVE_RELATIVE_WINDOWS
raise(InvalidPattern, "A PathPattern cannot be a Windows current drive relative path.")
end
return stripped
end

def initialize(pattern)
stripped = validate(pattern)
begin
@pathname = Pathname.new(stripped)
rescue ArgumentError => error
raise InvalidPattern.new("PathPatterns cannot be created with a zero byte.", error)
end
end
end

class RelativePathPattern < PathPattern
def absolute?
false
end

def validate(pattern)
stripped = super(pattern)
case stripped
when ABSOLUTE_WINDOWS
raise(InvalidPattern, "A relative PathPattern cannot be prefixed with a drive.")
when ABSOLUTE_UNIX
raise(InvalidPattern, "A relative PathPattern cannot be an absolute path.")
end
return stripped
end
end

class AbsolutePathPattern < PathPattern
def absolute?
true
end

def validate(pattern)
stripped = super(pattern)
if stripped !~ ABSOLUTE_UNIX and stripped !~ ABSOLUTE_WINDOWS
raise(InvalidPattern, "An absolute PathPattern cannot be a relative path.")
end
stripped
end
end
end
23 changes: 19 additions & 4 deletions lib/puppet/module.rb
Expand Up @@ -11,6 +11,7 @@ class UnsupportedPlatform < Error; end
class IncompatiblePlatform < Error; end
class MissingMetadata < Error; end
class InvalidName < Error; end
class InvalidFilePattern < Error; end

include Puppet::Util::Logging

Expand Down Expand Up @@ -132,10 +133,24 @@ def load_metadata
# Return the list of manifests matching the given glob pattern,
# defaulting to 'init.{pp,rb}' for empty modules.
def match_manifests(rest)
pat = File.join(path, "manifests", rest || 'init')
[manifest("init.pp"),manifest("init.rb")].compact + Dir.
glob(pat + (File.extname(pat).empty? ? '.{pp,rb}' : '')).
reject { |f| FileTest.directory?(f) }
relative_pattern = if rest
File.extname(rest).empty? ? "#{rest}.{pp,rb}" : rest
else
'init.{pp,rb}'
end
pattern = Puppet::FileSystem::PathPattern.relative(relative_pattern)
manifests = Puppet::FileSystem::PathPattern.absolute(File.join(path, "manifests"))

wanted_manifests = pattern.prefix_with(manifests)

init_manifests = [manifest("init.pp"), manifest("init.rb")].compact
searched_manifests = wanted_manifests.glob.reject { |f| FileTest.directory?(f) }

init_manifests + searched_manifests
rescue Puppet::FileSystem::PathPattern::InvalidPattern => error
raise Puppet::Module::InvalidFilePattern.new(
"The pattern \"#{rest}\" to find manifests in the module \"#{name}\" " +
"is invalid and potentially unsafe.", error)
end

def metadata_file
Expand Down
37 changes: 21 additions & 16 deletions lib/puppet/parser/files.rb
Expand Up @@ -6,26 +6,28 @@
# doesn't really belong in the Puppet::Module class,
# but it doesn't really belong anywhere else, either.
module Puppet; module Parser; module Files

module_function

# Return a list of manifests (as absolute filenames) that match +pat+
# with the current directory set to +cwd+. If the first component of
# +pat+ does not contain any wildcards and is an existing module, return
# a list of manifests in that module matching the rest of +pat+
# Otherwise, try to find manifests matching +pat+ relative to +cwd+
def find_manifests(start, options = {})
cwd = options[:cwd] || Dir.getwd
module_name, pattern = split_file_path(start)
# Return a list of manifests as absolute filenames matching the given
# pattern.
#
# @param pattern [String] A reference for a file in a module. It is the format "<modulename>/<file glob>"
# @param environment [Puppet::Node::Environment] the environment of modules
#
# @return [Array(String, Array<String>)] the module name and the list of files found
# @api private
def find_manifests_in_modules(pattern, environment)
module_name, file_pattern = split_file_path(pattern)
begin
if mod = Puppet::Module.find(module_name, options[:environment])
return [mod.name, mod.match_manifests(pattern)]
if mod = Puppet::Module.find(module_name, environment)
return [mod.name, mod.match_manifests(file_pattern)]
end
rescue Puppet::Module::InvalidName
# Than that would be a "no."
# one of the modules being loaded might have an invalid name and so
# looking for one might blow up since we load them lazily.
end
abspat = File::expand_path(start, cwd)
[nil, Dir.glob(abspat + (File.extname(abspat).empty? ? '{.pp,.rb}' : '' )).uniq.reject { |f| FileTest.directory?(f) }]
[nil, []]
end

# Find the concrete file denoted by +file+. If +file+ is absolute,
Expand Down Expand Up @@ -81,9 +83,12 @@ def templatepath(environment = nil)

# Split the path into the module and the rest of the path, or return
# nil if the path is empty or absolute (starts with a /).
# This method can return nil & anyone calling it needs to handle that.
def split_file_path(path)
path.split(File::SEPARATOR, 2) unless path == "" or Puppet::Util.absolute_path?(path)
if path == "" or Puppet::Util.absolute_path?(path)
nil
else
path.split(File::SEPARATOR, 2)
end
end

end; end; end
11 changes: 10 additions & 1 deletion lib/puppet/parser/parser_support.rb
Expand Up @@ -98,7 +98,16 @@ def file=(file)
def_delegators :known_resource_types, :watch_file, :version

def import(file)
known_resource_types.loader.import(file, @lexer.file)
if @lexer.file
# use a path relative to the file doing the importing
dir = File.dirname(@lexer.file)
else
# otherwise assume that everything needs to be from where the user is
# executing this command. Normally, this would be in a "puppet apply -e"
dir = Dir.pwd
end

known_resource_types.loader.import(file, dir)
end

def initialize(env)
Expand Down
81 changes: 51 additions & 30 deletions lib/puppet/parser/type_loader.rb
Expand Up @@ -62,50 +62,46 @@ def do_once(file)
end
end

# Import our files.
def import(file, current_file = nil)
# Import manifest files that match a given file glob pattern.
#
# @param pattern [String] the file glob to apply when determining which files
# to load
# @param dir [String] base directory to use when the file is not
# found in a module
# @api private
def import(pattern, dir)
return if Puppet[:ignoreimport]

# use a path relative to the file doing the importing
if current_file
dir = current_file.sub(%r{[^/]+$},'').sub(/\/$/, '')
else
dir = "."
end
if dir == ""
dir = "."
end
modname, files = Puppet::Parser::Files.find_manifests_in_modules(pattern, environment)
if files.empty?
abspat = File.expand_path(pattern, dir)
file_pattern = abspat + (File.extname(abspat).empty? ? '{.pp,.rb}' : '' )

pat = file
modname, files = Puppet::Parser::Files.find_manifests(pat, :cwd => dir, :environment => environment)
if files.size == 0
raise Puppet::ImportError.new("No file(s) found for import of '#{pat}'")
end
files = Dir.glob(file_pattern).uniq.reject { |f| FileTest.directory?(f) }
modname = nil

loaded_asts = []
files.each do |file|
unless Puppet::Util.absolute_path?(file)
file = File.join(dir, file)
if files.empty?
raise_no_files_found(pattern)
end
@loading_helper.do_once(file) do
loaded_asts << parse_file(file)
end
end
loaded_asts.inject([]) do |loaded_types, ast|
loaded_types + known_resource_types.import_ast(ast, modname)
end

load_files(modname, files)
end

# Load all of the manifest files in all known modules.
# @api private
def import_all
# And then load all files from each module, but (relying on system
# behavior) only load files from the first module of a given name. E.g.,
# given first/foo and second/foo, only files from first/foo will be loaded.
environment.modules.each do |mod|
manifest_files = []
Find.find(mod.manifests) do |path|
if path =~ /\.pp$/ or path =~ /\.rb$/
import(path)
if path.end_with?(".pp") || path.end_with?(".rb")
manifest_files << path
end
end
load_files(mod.name, manifest_files)
end
end

Expand All @@ -121,7 +117,7 @@ def try_load_fqname(type, fqname)
return nil if fqname == "" # special-case main.
name2files(fqname).each do |filename|
begin
imported_types = import(filename)
imported_types = import_from_modules(filename)
if result = imported_types.find { |t| t.type == type and t.name == fqname }
Puppet.debug "Automatically imported #{fqname} from #{filename} into #{environment}"
return result
Expand All @@ -138,14 +134,39 @@ def try_load_fqname(type, fqname)

def parse_file(file)
Puppet.debug("importing '#{file}' in environment #{environment}")
# parser = Puppet::Parser::Parser.new(environment)
parser = Puppet::Parser::ParserFactory.parser(environment)
parser.file = file
return parser.parse
end

private

def import_from_modules(pattern)
modname, files = Puppet::Parser::Files.find_manifests_in_modules(pattern, environment)
if files.empty?
raise_no_files_found(pattern)
end

load_files(modname, files)
end

def raise_no_files_found(pattern)
raise Puppet::ImportError, "No file(s) found for import of '#{pattern}'"
end

def load_files(modname, files)
loaded_asts = []
files.each do |file|
@loading_helper.do_once(file) do
loaded_asts << parse_file(file)
end
end

loaded_asts.collect do |ast|
known_resource_types.import_ast(ast, modname)
end.flatten
end

# Return a list of all file basenames that should be tried in order
# to load the object with the given fully qualified name.
def name2files(fqname)
Expand Down

0 comments on commit a177c9d

Please sign in to comment.