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:
parent
00e089e503
commit
84759faa09
8 changed files with 178 additions and 65 deletions
|
@ -856,6 +856,8 @@ class AnsibleModule(object):
|
|||
(k, v) = x.split("=",1)
|
||||
except Exception, e:
|
||||
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
|
||||
params2 = json.loads(MODULE_COMPLEX_ARGS)
|
||||
params2.update(params)
|
||||
|
|
|
@ -31,6 +31,7 @@ import sys
|
|||
import pipes
|
||||
import jinja2
|
||||
import subprocess
|
||||
import shlex
|
||||
import getpass
|
||||
|
||||
import ansible.constants as C
|
||||
|
@ -388,6 +389,35 @@ class Runner(object):
|
|||
|
||||
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 = template.template(basedir, items_terms, 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:
|
||||
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
|
||||
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)
|
||||
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)
|
||||
except jinja2.exceptions.UndefinedError, e:
|
||||
raise errors.AnsibleUndefinedVariable("One or more undefined variables: %s" % str(e))
|
||||
|
|
|
@ -122,14 +122,25 @@ class ActionModule(object):
|
|||
self.runner._remote_chmod(conn, 'a+r', xfered, tmp)
|
||||
|
||||
# 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):
|
||||
return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=src, after=resultant))
|
||||
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)
|
||||
return res
|
||||
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)))
|
||||
return self.runner._execute_module(conn, tmp, 'file', module_args, inject=inject)
|
||||
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)
|
||||
|
||||
return self.runner._execute_module(conn, tmp, 'file', module_args_tmp, inject=inject)
|
||||
|
|
|
@ -238,11 +238,16 @@ class ActionModule(object):
|
|||
|
||||
# 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
|
||||
module_args_tmp = "%s src=%s dest=%s original_basename=%s" % (module_args,
|
||||
pipes.quote(tmp_src), pipes.quote(dest), pipes.quote(source_rel))
|
||||
new_module_args = dict(
|
||||
src=tmp_src,
|
||||
dest=dest,
|
||||
original_basename=source_rel
|
||||
)
|
||||
|
||||
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_executed = True
|
||||
|
@ -260,12 +265,16 @@ class ActionModule(object):
|
|||
tmp_src = tmp_path + source_rel
|
||||
|
||||
# Build temporary module_args.
|
||||
module_args_tmp = "%s src=%s original_basename=%s" % (module_args,
|
||||
pipes.quote(tmp_src), pipes.quote(source_rel))
|
||||
new_module_args = dict(
|
||||
src=tmp_src,
|
||||
dest=dest,
|
||||
)
|
||||
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:
|
||||
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.
|
||||
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)
|
||||
|
|
|
@ -117,12 +117,17 @@ class ActionModule(object):
|
|||
self.runner._remote_chmod(conn, 'a+r', xfered, tmp)
|
||||
|
||||
# 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):
|
||||
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:
|
||||
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):
|
||||
res.diff = dict(before=dest_contents, after=resultant)
|
||||
return res
|
||||
|
|
|
@ -53,6 +53,10 @@ VERBOSITY=0
|
|||
|
||||
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:
|
||||
import json
|
||||
except ImportError:
|
||||
|
@ -341,38 +345,44 @@ def 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 '''
|
||||
data = orig_data
|
||||
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)
|
||||
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
|
||||
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):
|
||||
data = orig_data.copy()
|
||||
for key in data:
|
||||
new_key = _clean_data_struct(key)
|
||||
new_val = _clean_data_struct(data[key])
|
||||
new_key = _clean_data_struct(key, from_remote, from_inventory)
|
||||
new_val = _clean_data_struct(data[key], from_remote, from_inventory)
|
||||
if key != new_key:
|
||||
del data[key]
|
||||
data[new_key] = new_val
|
||||
elif isinstance(orig_data, list):
|
||||
data = orig_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):
|
||||
data = _clean_data(orig_data)
|
||||
data = _clean_data(orig_data, from_remote, from_inventory)
|
||||
else:
|
||||
data = orig_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 '''
|
||||
|
||||
orig_data = raw_data
|
||||
|
@ -407,10 +417,31 @@ def parse_json(raw_data, from_remote=False):
|
|||
return { "failed" : True, "parsed" : False, "msg" : orig_data }
|
||||
|
||||
if from_remote:
|
||||
results = _clean_data_struct(results)
|
||||
results = _clean_data_struct(results, from_remote, from_inventory)
|
||||
|
||||
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):
|
||||
''' smush Jinaj2 braces so unresolved templates like {{ foo }} don't get parsed weird by key=value code '''
|
||||
while '{{ ' in data:
|
||||
|
@ -641,7 +672,7 @@ def parse_kv(args):
|
|||
for x in vargs:
|
||||
if "=" in x:
|
||||
k, v = x.split("=",1)
|
||||
options[k]=v
|
||||
options[k] = v
|
||||
return options
|
||||
|
||||
def merge_hash(a, b):
|
||||
|
@ -1089,11 +1120,14 @@ def list_intersection(a, b):
|
|||
|
||||
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
|
||||
where Jinja2 would return a string
|
||||
but we do not want to allow it to call functions (outside of Jinja2, where
|
||||
the env is constrained)
|
||||
|
||||
Where Jinja2 would return a string but we do not want to allow it to
|
||||
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:
|
||||
http://stackoverflow.com/questions/12523516/using-ast-and-whitelists-to-make-pythons-eval-safe
|
||||
|
|
|
@ -193,29 +193,36 @@ class CommandModule(AnsibleModule):
|
|||
args = args.replace("#USE_SHELL", "")
|
||||
params['shell'] = True
|
||||
|
||||
r = re.compile(r'(^|\s)(creates|removes|chdir|executable|NO_LOG)=(?P<quote>[\'"])?(.*?)(?(quote)(?<!\\)(?P=quote))((?<!\\)(?=\s)|$)')
|
||||
for m in r.finditer(args):
|
||||
v = m.group(4).replace("\\", "")
|
||||
if m.group(2) == "creates":
|
||||
params['creates'] = v
|
||||
elif m.group(2) == "removes":
|
||||
params['removes'] = v
|
||||
elif m.group(2) == "chdir":
|
||||
v = os.path.expanduser(v)
|
||||
v = os.path.abspath(v)
|
||||
# use shlex to split up the args, while being careful to preserve
|
||||
# single quotes so they're not removed accidentally
|
||||
lexer = shlex.shlex(args, posix=True)
|
||||
lexer.whitespace_split = True
|
||||
lexer.quotes = '"'
|
||||
lexer.ignore_quotes = "'"
|
||||
items = list(lexer)
|
||||
|
||||
command_args = ''
|
||||
for x in items:
|
||||
if '=' in x:
|
||||
# check to see if this is a special parameter for the command
|
||||
k, v = x.split('=', 1)
|
||||
if k in ('creates', 'removes', 'chdir', 'executable', 'NO_LOG'):
|
||||
if k == "chdir":
|
||||
v = os.path.abspath(os.path.expanduser(v))
|
||||
if not (os.path.exists(v) and os.path.isdir(v)):
|
||||
self.fail_json(rc=258, msg="cannot change to directory '%s': path does not exist" % v)
|
||||
params['chdir'] = v
|
||||
elif m.group(2) == "executable":
|
||||
v = os.path.expanduser(v)
|
||||
v = os.path.abspath(v)
|
||||
elif k == "executable":
|
||||
v = os.path.abspath(os.path.expanduser(v))
|
||||
if not (os.path.exists(v)):
|
||||
self.fail_json(rc=258, msg="cannot use executable '%s': file does not exist" % v)
|
||||
params['executable'] = v
|
||||
elif m.group(2) == "NO_LOG":
|
||||
params['NO_LOG'] = self.boolean(v)
|
||||
args = r.sub("", args)
|
||||
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'])
|
||||
|
||||
main()
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
# WITH_ITEMS
|
||||
|
||||
- name: test with_items
|
||||
set_fact: "{{ item + '=moo' }}"
|
||||
set_fact: "{{ item }}=moo"
|
||||
with_items:
|
||||
- 'foo'
|
||||
- 'bar'
|
||||
|
@ -36,7 +36,7 @@
|
|||
# WITH_NESTED
|
||||
|
||||
- name: test with_nested
|
||||
set_fact: "{{ item.0 + item.1 + '=x' }}"
|
||||
set_fact: "{{ item.0 + item.1 }}=x"
|
||||
with_nested:
|
||||
- [ 'a', 'b' ]
|
||||
- [ 'c', 'd' ]
|
||||
|
@ -57,7 +57,7 @@
|
|||
# WITH_SEQUENCE
|
||||
|
||||
- name: test with_sequence
|
||||
set_fact: "{{ 'x' + item + '=' + item }}"
|
||||
set_fact: "{{ 'x' + item }}={{ item }}"
|
||||
with_sequence: start=0 end=3
|
||||
|
||||
- name: verify with_sequence
|
||||
|
@ -71,7 +71,7 @@
|
|||
# WITH_RANDOM_CHOICE
|
||||
|
||||
- name: test with_random_choice
|
||||
set_fact: "{{ 'random=' + item }}"
|
||||
set_fact: "random={{ item }}"
|
||||
with_random_choice:
|
||||
- "foo"
|
||||
- "bar"
|
||||
|
@ -84,7 +84,7 @@
|
|||
# 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:
|
||||
- element_data
|
||||
- the_list
|
||||
|
@ -101,7 +101,7 @@
|
|||
|
||||
- name: test with_together
|
||||
#shell: echo {{ item }}
|
||||
set_fact: "{{ item.0 + '=' + item.1 }}"
|
||||
set_fact: "{{ item.0 }}={{ item.1 }}"
|
||||
with_together:
|
||||
- [ 'a', 'b', 'c', 'd' ]
|
||||
- [ '1', '2', '3', '4' ]
|
||||
|
@ -124,7 +124,7 @@
|
|||
|
||||
- name: test with_first_found
|
||||
#shell: echo {{ item }}
|
||||
set_fact: "{{ 'first_found=' + item }}"
|
||||
set_fact: "first_found={{ item }}"
|
||||
with_first_found:
|
||||
- "{{ output_dir + '/does_not_exist' }}"
|
||||
- "{{ output_dir + '/foo1' }}"
|
||||
|
@ -146,7 +146,7 @@
|
|||
|
||||
- name: test with_lines
|
||||
#shell: echo "{{ item }}"
|
||||
set_fact: "{{ item + '=set' }}"
|
||||
set_fact: "{{ item }}=set"
|
||||
with_lines: for i in $(seq 1 5); do echo "l$i" ; done;
|
||||
|
||||
- name: verify with_lines results
|
||||
|
@ -164,7 +164,7 @@
|
|||
register: list_data
|
||||
|
||||
- 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
|
||||
|
||||
- name: verify with_indexed_items result
|
||||
|
@ -179,8 +179,7 @@
|
|||
# WITH_FLATTENED
|
||||
|
||||
- name: test with_flattened
|
||||
#shell: echo {{ item + "test" }}
|
||||
set_fact: "{{ item + '=flattened' }}"
|
||||
set_fact: "{{ item }}=flattened"
|
||||
with_flattened:
|
||||
- [ 'a__' ]
|
||||
- [ 'b__', ['c__', 'd__'] ]
|
||||
|
|
Loading…
Reference in a new issue