1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Security fixes:

* Strip lookup calls out of inventory variables and clean unsafe data
  returned from lookup plugins (CVE-2014-4966)
* Make sure vars don't insert extra parameters into module args and prevent
  duplicate params from superseding previous params (CVE-2014-4967)
This commit is contained in:
James Cammarata 2014-07-21 11:20:49 -05:00
parent 00e089e503
commit 84759faa09
8 changed files with 178 additions and 65 deletions

View file

@ -856,6 +856,8 @@ class AnsibleModule(object):
(k, v) = x.split("=",1) (k, v) = x.split("=",1)
except Exception, e: except Exception, e:
self.fail_json(msg="this module requires key=value arguments (%s)" % (items)) self.fail_json(msg="this module requires key=value arguments (%s)" % (items))
if k in params:
self.fail_json(msg="duplicate parameter: %s (value=%s)" % (k, v))
params[k] = v params[k] = v
params2 = json.loads(MODULE_COMPLEX_ARGS) params2 = json.loads(MODULE_COMPLEX_ARGS)
params2.update(params) params2.update(params)

View file

@ -31,6 +31,7 @@ import sys
import pipes import pipes
import jinja2 import jinja2
import subprocess import subprocess
import shlex
import getpass import getpass
import ansible.constants as C import ansible.constants as C
@ -388,6 +389,35 @@ class Runner(object):
return actual_user return actual_user
def _count_module_args(self, args):
'''
Count the number of k=v pairs in the supplied module args. This is
basically a specialized version of parse_kv() from utils with a few
minor changes.
'''
options = {}
if args is not None:
args = args.encode('utf-8')
try:
lexer = shlex.shlex(args, posix=True)
lexer.whitespace_split = True
lexer.quotes = '"'
lexer.ignore_quotes = "'"
vargs = list(lexer)
except ValueError, ve:
if 'no closing quotation' in str(ve).lower():
raise errors.AnsibleError("error parsing argument string '%s', try quoting the entire line." % args)
else:
raise
vargs = [x.decode('utf-8') for x in vargs]
for x in vargs:
if "=" in x:
k, v = x.split("=",1)
if k in options:
raise errors.AnsibleError("a duplicate parameter was found in the argument string (%s)" % k)
options[k] = v
return len(options)
# ***************************************************** # *****************************************************
@ -604,6 +634,9 @@ class Runner(object):
items_terms = self.module_vars.get('items_lookup_terms', '') items_terms = self.module_vars.get('items_lookup_terms', '')
items_terms = template.template(basedir, items_terms, inject) items_terms = template.template(basedir, items_terms, inject)
items = utils.plugins.lookup_loader.get(items_plugin, runner=self, basedir=basedir).run(items_terms, inject=inject) items = utils.plugins.lookup_loader.get(items_plugin, runner=self, basedir=basedir).run(items_terms, inject=inject)
# strip out any jinja2 template syntax within
# the data returned by the lookup plugin
items = utils._clean_data_struct(items, from_remote=True)
if type(items) != list: if type(items) != list:
raise errors.AnsibleError("lookup plugins have to return a list: %r" % items) raise errors.AnsibleError("lookup plugins have to return a list: %r" % items)
@ -826,7 +859,20 @@ class Runner(object):
# render module_args and complex_args templates # render module_args and complex_args templates
try: try:
# When templating module_args, we need to be careful to ensure
# that no variables inadvertantly (or maliciously) add params
# to the list of args. We do this by counting the number of k=v
# pairs before and after templating.
num_args_pre = self._count_module_args(module_args)
module_args = template.template(self.basedir, module_args, inject, fail_on_undefined=self.error_on_undefined_vars) module_args = template.template(self.basedir, module_args, inject, fail_on_undefined=self.error_on_undefined_vars)
num_args_post = self._count_module_args(module_args)
if num_args_pre != num_args_post:
raise errors.AnsibleError("A variable inserted a new parameter into the module args. " + \
"Be sure to quote variables if they contain equal signs (for example: \"{{var}}\").")
# And we also make sure nothing added in special flags for things
# like the command/shell module (ie. #USE_SHELL)
if '#USE_SHELL' in module_args:
raise errors.AnsibleError("A variable tried to add #USE_SHELL to the module arguments.")
complex_args = template.template(self.basedir, complex_args, inject, fail_on_undefined=self.error_on_undefined_vars) complex_args = template.template(self.basedir, complex_args, inject, fail_on_undefined=self.error_on_undefined_vars)
except jinja2.exceptions.UndefinedError, e: except jinja2.exceptions.UndefinedError, e:
raise errors.AnsibleUndefinedVariable("One or more undefined variables: %s" % str(e)) raise errors.AnsibleUndefinedVariable("One or more undefined variables: %s" % str(e))

View file

@ -122,14 +122,25 @@ class ActionModule(object):
self.runner._remote_chmod(conn, 'a+r', xfered, tmp) self.runner._remote_chmod(conn, 'a+r', xfered, tmp)
# run the copy module # run the copy module
module_args = "%s src=%s dest=%s original_basename=%s" % (module_args, pipes.quote(xfered), pipes.quote(dest), pipes.quote(os.path.basename(src))) new_module_args = dict(
src=xfered,
dest=dest,
original_basename=os.path.basename(src),
)
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
if self.runner.noop_on_check(inject): if self.runner.noop_on_check(inject):
return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=src, after=resultant)) return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=src, after=resultant))
else: else:
res = self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject) res = self.runner._execute_module(conn, tmp, 'copy', module_args_tmp, inject=inject)
res.diff = dict(after=resultant) res.diff = dict(after=resultant)
return res return res
else: else:
module_args = "%s src=%s dest=%s original_basename=%s" % (module_args, pipes.quote(xfered), pipes.quote(dest), pipes.quote(os.path.basename(src))) new_module_args = dict(
return self.runner._execute_module(conn, tmp, 'file', module_args, inject=inject) src=xfered,
dest=dest,
original_basename=os.path.basename(src),
)
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
return self.runner._execute_module(conn, tmp, 'file', module_args_tmp, inject=inject)

View file

@ -238,11 +238,16 @@ class ActionModule(object):
# src and dest here come after original and override them # src and dest here come after original and override them
# we pass dest only to make sure it includes trailing slash in case of recursive copy # we pass dest only to make sure it includes trailing slash in case of recursive copy
module_args_tmp = "%s src=%s dest=%s original_basename=%s" % (module_args, new_module_args = dict(
pipes.quote(tmp_src), pipes.quote(dest), pipes.quote(source_rel)) src=tmp_src,
dest=dest,
original_basename=source_rel
)
if self.runner.no_log: if self.runner.no_log:
module_args_tmp = "%s NO_LOG=True" % module_args_tmp new_module_args['NO_LOG'] = True
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
module_return = self.runner._execute_module(conn, tmp_path, 'copy', module_args_tmp, inject=inject, complex_args=complex_args, delete_remote_tmp=delete_remote_tmp) module_return = self.runner._execute_module(conn, tmp_path, 'copy', module_args_tmp, inject=inject, complex_args=complex_args, delete_remote_tmp=delete_remote_tmp)
module_executed = True module_executed = True
@ -260,12 +265,16 @@ class ActionModule(object):
tmp_src = tmp_path + source_rel tmp_src = tmp_path + source_rel
# Build temporary module_args. # Build temporary module_args.
module_args_tmp = "%s src=%s original_basename=%s" % (module_args, new_module_args = dict(
pipes.quote(tmp_src), pipes.quote(source_rel)) src=tmp_src,
dest=dest,
)
if self.runner.noop_on_check(inject): if self.runner.noop_on_check(inject):
module_args_tmp = "%s CHECKMODE=True" % module_args_tmp new_module_args['CHECKMODE'] = True
if self.runner.no_log: if self.runner.no_log:
module_args_tmp = "%s NO_LOG=True" % module_args_tmp new_module_args['NO_LOG'] = True
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
# Execute the file module. # Execute the file module.
module_return = self.runner._execute_module(conn, tmp_path, 'file', module_args_tmp, inject=inject, complex_args=complex_args, delete_remote_tmp=delete_remote_tmp) module_return = self.runner._execute_module(conn, tmp_path, 'file', module_args_tmp, inject=inject, complex_args=complex_args, delete_remote_tmp=delete_remote_tmp)

View file

@ -117,12 +117,17 @@ class ActionModule(object):
self.runner._remote_chmod(conn, 'a+r', xfered, tmp) self.runner._remote_chmod(conn, 'a+r', xfered, tmp)
# run the copy module # run the copy module
module_args = "%s src=%s dest=%s original_basename=%s" % (module_args, pipes.quote(xfered), pipes.quote(dest), pipes.quote(os.path.basename(source))) new_module_args = dict(
src=xfered,
dest=dest,
original_basename=os.path.basename(source),
)
module_args_tmp = utils.merge_module_args(module_args, new_module_args)
if self.runner.noop_on_check(inject): if self.runner.noop_on_check(inject):
return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=source, before=dest_contents, after=resultant)) return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=source, before=dest_contents, after=resultant))
else: else:
res = self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject, complex_args=complex_args) res = self.runner._execute_module(conn, tmp, 'copy', module_args_tmp, inject=inject, complex_args=complex_args)
if res.result.get('changed', False): if res.result.get('changed', False):
res.diff = dict(before=dest_contents, after=resultant) res.diff = dict(before=dest_contents, after=resultant)
return res return res

View file

@ -53,6 +53,10 @@ VERBOSITY=0
MAX_FILE_SIZE_FOR_DIFF=1*1024*1024 MAX_FILE_SIZE_FOR_DIFF=1*1024*1024
# caching the compilation of the regex used
# to check for lookup calls within data
LOOKUP_REGEX=re.compile(r'lookup\s*\(')
try: try:
import json import json
except ImportError: except ImportError:
@ -341,38 +345,44 @@ def json_loads(data):
return json.loads(data) return json.loads(data)
def _clean_data(orig_data): def _clean_data(orig_data, from_remote=False, from_inventory=False):
''' remove template tags from a string ''' ''' remove template tags from a string '''
data = orig_data data = orig_data
if isinstance(orig_data, basestring): if isinstance(orig_data, basestring):
for pattern,replacement in (('{{','{#'), ('}}','#}'), ('{%','{#'), ('%}','#}')): sub_list = [('{%','{#'), ('%}','#}')]
if from_remote or (from_inventory and '{{' in data and LOOKUP_REGEX.search(data)):
# if from a remote, we completely disable any jinja2 blocks
sub_list.extend([('{{','{#'), ('}}','#}')])
for pattern,replacement in sub_list:
data = data.replace(pattern, replacement) data = data.replace(pattern, replacement)
return data return data
def _clean_data_struct(orig_data): def _clean_data_struct(orig_data, from_remote=False, from_inventory=False):
''' '''
walk a complex data structure, and use _clean_data() to walk a complex data structure, and use _clean_data() to
remove any template tags that may exist remove any template tags that may exist
''' '''
if not from_remote and not from_inventory:
raise errors.AnsibleErrors("when cleaning data, you must specify either from_remote or from_inventory")
if isinstance(orig_data, dict): if isinstance(orig_data, dict):
data = orig_data.copy() data = orig_data.copy()
for key in data: for key in data:
new_key = _clean_data_struct(key) new_key = _clean_data_struct(key, from_remote, from_inventory)
new_val = _clean_data_struct(data[key]) new_val = _clean_data_struct(data[key], from_remote, from_inventory)
if key != new_key: if key != new_key:
del data[key] del data[key]
data[new_key] = new_val data[new_key] = new_val
elif isinstance(orig_data, list): elif isinstance(orig_data, list):
data = orig_data[:] data = orig_data[:]
for i in range(0, len(data)): for i in range(0, len(data)):
data[i] = _clean_data_struct(data[i]) data[i] = _clean_data_struct(data[i], from_remote, from_inventory)
elif isinstance(orig_data, basestring): elif isinstance(orig_data, basestring):
data = _clean_data(orig_data) data = _clean_data(orig_data, from_remote, from_inventory)
else: else:
data = orig_data data = orig_data
return data return data
def parse_json(raw_data, from_remote=False): def parse_json(raw_data, from_remote=False, from_inventory=False):
''' this version for module return data only ''' ''' this version for module return data only '''
orig_data = raw_data orig_data = raw_data
@ -407,10 +417,31 @@ def parse_json(raw_data, from_remote=False):
return { "failed" : True, "parsed" : False, "msg" : orig_data } return { "failed" : True, "parsed" : False, "msg" : orig_data }
if from_remote: if from_remote:
results = _clean_data_struct(results) results = _clean_data_struct(results, from_remote, from_inventory)
return results return results
def merge_module_args(current_args, new_args):
'''
merges either a dictionary or string of k=v pairs with another string of k=v pairs,
and returns a new k=v string without duplicates.
'''
if not isinstance(current_args, basestring):
raise errors.AnsibleError("expected current_args to be a basestring")
# we use parse_kv to split up the current args into a dictionary
final_args = parse_kv(current_args)
if isinstance(new_args, dict):
final_args.update(new_args)
elif isinstance(new_args, basestring):
new_args_kv = parse_kv(new_args)
final_args.update(new_args_kv)
# then we re-assemble into a string
module_args = ""
for (k,v) in final_args.iteritems():
if isinstance(v, basestring):
module_args = "%s=%s %s" % (k, pipes.quote(v), module_args)
return module_args.strip()
def smush_braces(data): def smush_braces(data):
''' smush Jinaj2 braces so unresolved templates like {{ foo }} don't get parsed weird by key=value code ''' ''' smush Jinaj2 braces so unresolved templates like {{ foo }} don't get parsed weird by key=value code '''
while '{{ ' in data: while '{{ ' in data:
@ -641,7 +672,7 @@ def parse_kv(args):
for x in vargs: for x in vargs:
if "=" in x: if "=" in x:
k, v = x.split("=",1) k, v = x.split("=",1)
options[k]=v options[k] = v
return options return options
def merge_hash(a, b): def merge_hash(a, b):
@ -1089,11 +1120,14 @@ def list_intersection(a, b):
def safe_eval(expr, locals={}, include_exceptions=False): def safe_eval(expr, locals={}, include_exceptions=False):
''' '''
this is intended for allowing things like: This is intended for allowing things like:
with_items: a_list_variable with_items: a_list_variable
where Jinja2 would return a string
but we do not want to allow it to call functions (outside of Jinja2, where Where Jinja2 would return a string but we do not want to allow it to
the env is constrained) call functions (outside of Jinja2, where the env is constrained). If
the input data to this function came from an untrusted (remote) source,
it should first be run through _clean_data_struct() to ensure the data
is further sanitized prior to evaluation.
Based on: Based on:
http://stackoverflow.com/questions/12523516/using-ast-and-whitelists-to-make-pythons-eval-safe http://stackoverflow.com/questions/12523516/using-ast-and-whitelists-to-make-pythons-eval-safe

View file

@ -184,38 +184,45 @@ class CommandModule(AnsibleModule):
''' read the input and return a dictionary and the arguments string ''' ''' read the input and return a dictionary and the arguments string '''
args = MODULE_ARGS args = MODULE_ARGS
params = {} params = {}
params['chdir'] = None params['chdir'] = None
params['creates'] = None params['creates'] = None
params['removes'] = None params['removes'] = None
params['shell'] = False params['shell'] = False
params['executable'] = None params['executable'] = None
if "#USE_SHELL" in args: if "#USE_SHELL" in args:
args = args.replace("#USE_SHELL", "") args = args.replace("#USE_SHELL", "")
params['shell'] = True params['shell'] = True
r = re.compile(r'(^|\s)(creates|removes|chdir|executable|NO_LOG)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)') # use shlex to split up the args, while being careful to preserve
for m in r.finditer(args): # single quotes so they're not removed accidentally
v = m.group(4).replace("\\", "") lexer = shlex.shlex(args, posix=True)
if m.group(2) == "creates": lexer.whitespace_split = True
params['creates'] = v lexer.quotes = '"'
elif m.group(2) == "removes": lexer.ignore_quotes = "'"
params['removes'] = v items = list(lexer)
elif m.group(2) == "chdir":
v = os.path.expanduser(v) command_args = ''
v = os.path.abspath(v) for x in items:
if not (os.path.exists(v) and os.path.isdir(v)): if '=' in x:
self.fail_json(rc=258, msg="cannot change to directory '%s': path does not exist" % v) # check to see if this is a special parameter for the command
params['chdir'] = v k, v = x.split('=', 1)
elif m.group(2) == "executable": if k in ('creates', 'removes', 'chdir', 'executable', 'NO_LOG'):
v = os.path.expanduser(v) if k == "chdir":
v = os.path.abspath(v) v = os.path.abspath(os.path.expanduser(v))
if not (os.path.exists(v)): if not (os.path.exists(v) and os.path.isdir(v)):
self.fail_json(rc=258, msg="cannot use executable '%s': file does not exist" % v) self.fail_json(rc=258, msg="cannot change to directory '%s': path does not exist" % v)
params['executable'] = v elif k == "executable":
elif m.group(2) == "NO_LOG": v = os.path.abspath(os.path.expanduser(v))
params['NO_LOG'] = self.boolean(v) if not (os.path.exists(v)):
args = r.sub("", args) self.fail_json(rc=258, msg="cannot use executable '%s': file does not exist" % v)
params['args'] = args params[k] = v
else:
# this isn't a valid parameter, so just append it back to the list of arguments
command_args = "%s %s" % (command_args, x)
else:
# not a param, so just append it to the list of arguments
command_args = "%s %s" % (command_args, x)
params['args'] = command_args.strip()
return (params, params['args']) return (params, params['args'])
main() main()

View file

@ -19,7 +19,7 @@
# WITH_ITEMS # WITH_ITEMS
- name: test with_items - name: test with_items
set_fact: "{{ item + '=moo' }}" set_fact: "{{ item }}=moo"
with_items: with_items:
- 'foo' - 'foo'
- 'bar' - 'bar'
@ -36,7 +36,7 @@
# WITH_NESTED # WITH_NESTED
- name: test with_nested - name: test with_nested
set_fact: "{{ item.0 + item.1 + '=x' }}" set_fact: "{{ item.0 + item.1 }}=x"
with_nested: with_nested:
- [ 'a', 'b' ] - [ 'a', 'b' ]
- [ 'c', 'd' ] - [ 'c', 'd' ]
@ -57,7 +57,7 @@
# WITH_SEQUENCE # WITH_SEQUENCE
- name: test with_sequence - name: test with_sequence
set_fact: "{{ 'x' + item + '=' + item }}" set_fact: "{{ 'x' + item }}={{ item }}"
with_sequence: start=0 end=3 with_sequence: start=0 end=3
- name: verify with_sequence - name: verify with_sequence
@ -71,7 +71,7 @@
# WITH_RANDOM_CHOICE # WITH_RANDOM_CHOICE
- name: test with_random_choice - name: test with_random_choice
set_fact: "{{ 'random=' + item }}" set_fact: "random={{ item }}"
with_random_choice: with_random_choice:
- "foo" - "foo"
- "bar" - "bar"
@ -84,7 +84,7 @@
# WITH_SUBELEMENTS # WITH_SUBELEMENTS
- name: test with_subelements - name: test with_subelements
set_fact: "{{ '_'+ item.0.id + item.1 + '=' + item.1 }}" set_fact: "{{ '_'+ item.0.id + item.1 }}={{ item.1 }}"
with_subelements: with_subelements:
- element_data - element_data
- the_list - the_list
@ -101,7 +101,7 @@
- name: test with_together - name: test with_together
#shell: echo {{ item }} #shell: echo {{ item }}
set_fact: "{{ item.0 + '=' + item.1 }}" set_fact: "{{ item.0 }}={{ item.1 }}"
with_together: with_together:
- [ 'a', 'b', 'c', 'd' ] - [ 'a', 'b', 'c', 'd' ]
- [ '1', '2', '3', '4' ] - [ '1', '2', '3', '4' ]
@ -124,7 +124,7 @@
- name: test with_first_found - name: test with_first_found
#shell: echo {{ item }} #shell: echo {{ item }}
set_fact: "{{ 'first_found=' + item }}" set_fact: "first_found={{ item }}"
with_first_found: with_first_found:
- "{{ output_dir + '/does_not_exist' }}" - "{{ output_dir + '/does_not_exist' }}"
- "{{ output_dir + '/foo1' }}" - "{{ output_dir + '/foo1' }}"
@ -146,7 +146,7 @@
- name: test with_lines - name: test with_lines
#shell: echo "{{ item }}" #shell: echo "{{ item }}"
set_fact: "{{ item + '=set' }}" set_fact: "{{ item }}=set"
with_lines: for i in $(seq 1 5); do echo "l$i" ; done; with_lines: for i in $(seq 1 5); do echo "l$i" ; done;
- name: verify with_lines results - name: verify with_lines results
@ -164,7 +164,7 @@
register: list_data register: list_data
- name: create indexed list - name: create indexed list
set_fact: "{{ item[1] + item[0]|string + '=set' }}" set_fact: "{{ item[1] + item[0]|string }}=set"
with_indexed_items: list_data.stdout_lines with_indexed_items: list_data.stdout_lines
- name: verify with_indexed_items result - name: verify with_indexed_items result
@ -179,8 +179,7 @@
# WITH_FLATTENED # WITH_FLATTENED
- name: test with_flattened - name: test with_flattened
#shell: echo {{ item + "test" }} set_fact: "{{ item }}=flattened"
set_fact: "{{ item + '=flattened' }}"
with_flattened: with_flattened:
- [ 'a__' ] - [ 'a__' ]
- [ 'b__', ['c__', 'd__'] ] - [ 'b__', ['c__', 'd__'] ]