Fixing security bugs for CVE-2016-9587
This commit is contained in:
parent
258c6ada52
commit
a6fff93967
7 changed files with 129 additions and 47 deletions
|
@ -28,8 +28,10 @@ from ansible.errors import AnsibleError, AnsibleUndefinedVariable
|
||||||
from ansible.playbook.attribute import FieldAttribute
|
from ansible.playbook.attribute import FieldAttribute
|
||||||
from ansible.template import Templar
|
from ansible.template import Templar
|
||||||
from ansible.module_utils._text import to_native
|
from ansible.module_utils._text import to_native
|
||||||
|
from ansible.vars.unsafe_proxy import wrap_var
|
||||||
|
|
||||||
DEFINED_REGEX = re.compile(r'(hostvars\[.+\]|[\w_]+)\s+(not\s+is|is|is\s+not)\s+(defined|undefined)')
|
DEFINED_REGEX = re.compile(r'(hostvars\[.+\]|[\w_]+)\s+(not\s+is|is|is\s+not)\s+(defined|undefined)')
|
||||||
|
LOOKUP_REGEX = re.compile(r'lookup\s*\(')
|
||||||
|
|
||||||
class Conditional:
|
class Conditional:
|
||||||
|
|
||||||
|
@ -127,9 +129,12 @@ class Conditional:
|
||||||
return conditional
|
return conditional
|
||||||
|
|
||||||
# a Jinja2 evaluation that results in something Python can eval!
|
# a Jinja2 evaluation that results in something Python can eval!
|
||||||
|
if hasattr(conditional, '__UNSAFE__') and LOOKUP_REGEX.match(conditional):
|
||||||
|
raise AnsibleError("The conditional '%s' contains variables which came from an unsafe " \
|
||||||
|
"source and also contains a lookup() call, failing conditional check" % conditional)
|
||||||
|
|
||||||
presented = "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional
|
presented = "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional
|
||||||
conditional = templar.template(presented)
|
val = templar.template(presented).strip()
|
||||||
val = conditional.strip()
|
|
||||||
if val == "True":
|
if val == "True":
|
||||||
return True
|
return True
|
||||||
elif val == "False":
|
elif val == "False":
|
||||||
|
|
|
@ -30,7 +30,7 @@ import time
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
from ansible import constants as C
|
from ansible import constants as C
|
||||||
from ansible.compat.six import binary_type, text_type, iteritems, with_metaclass
|
from ansible.compat.six import binary_type, string_types, text_type, iteritems, with_metaclass
|
||||||
from ansible.compat.six.moves import shlex_quote
|
from ansible.compat.six.moves import shlex_quote
|
||||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure
|
from ansible.errors import AnsibleError, AnsibleConnectionFailure
|
||||||
from ansible.executor.module_common import modify_module
|
from ansible.executor.module_common import modify_module
|
||||||
|
@ -39,6 +39,7 @@ from ansible.module_utils.json_utils import _filter_non_json_lines
|
||||||
from ansible.parsing.utils.jsonify import jsonify
|
from ansible.parsing.utils.jsonify import jsonify
|
||||||
from ansible.playbook.play_context import MAGIC_VARIABLE_MAPPING
|
from ansible.playbook.play_context import MAGIC_VARIABLE_MAPPING
|
||||||
from ansible.release import __version__
|
from ansible.release import __version__
|
||||||
|
from ansible.vars.unsafe_proxy import wrap_var
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -458,6 +459,8 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||||
# happens sometimes when it is a dir and not on bsd
|
# happens sometimes when it is a dir and not on bsd
|
||||||
if 'checksum' not in mystat['stat']:
|
if 'checksum' not in mystat['stat']:
|
||||||
mystat['stat']['checksum'] = ''
|
mystat['stat']['checksum'] = ''
|
||||||
|
elif not isinstance(mystat['stat']['checksum'], string_types):
|
||||||
|
raise AnsibleError("Invalid checksum returned by stat: expected a string type but got %s" % type(mystat['stat']['checksum']))
|
||||||
|
|
||||||
return mystat['stat']
|
return mystat['stat']
|
||||||
|
|
||||||
|
@ -677,16 +680,9 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||||
display.debug("done with _execute_module (%s, %s)" % (module_name, module_args))
|
display.debug("done with _execute_module (%s, %s)" % (module_name, module_args))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _parse_returned_data(self, res):
|
def _clean_returned_data(self, data):
|
||||||
try:
|
|
||||||
filtered_output, warnings = _filter_non_json_lines(res.get('stdout', u''))
|
|
||||||
for w in warnings:
|
|
||||||
display.warning(w)
|
|
||||||
data = json.loads(filtered_output)
|
|
||||||
data['_ansible_parsed'] = True
|
|
||||||
if 'ansible_facts' in data and isinstance(data['ansible_facts'], dict):
|
|
||||||
remove_keys = set()
|
remove_keys = set()
|
||||||
fact_keys = set(data['ansible_facts'].keys())
|
fact_keys = set(data.keys())
|
||||||
# first we add all of our magic variable names to the set of
|
# first we add all of our magic variable names to the set of
|
||||||
# keys we want to remove from facts
|
# keys we want to remove from facts
|
||||||
for magic_var in MAGIC_VARIABLE_MAPPING:
|
for magic_var in MAGIC_VARIABLE_MAPPING:
|
||||||
|
@ -715,7 +711,21 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||||
# then we remove them (except for ssh host keys)
|
# then we remove them (except for ssh host keys)
|
||||||
for r_key in remove_keys:
|
for r_key in remove_keys:
|
||||||
if not r_key.startswith('ansible_ssh_host_key_'):
|
if not r_key.startswith('ansible_ssh_host_key_'):
|
||||||
del data['ansible_facts'][r_key]
|
del data[r_key]
|
||||||
|
|
||||||
|
def _parse_returned_data(self, res):
|
||||||
|
try:
|
||||||
|
filtered_output, warnings = _filter_non_json_lines(res.get('stdout', u''))
|
||||||
|
for w in warnings:
|
||||||
|
display.warning(w)
|
||||||
|
data = json.loads(filtered_output)
|
||||||
|
data['_ansible_parsed'] = True
|
||||||
|
if 'ansible_facts' in data and isinstance(data['ansible_facts'], dict):
|
||||||
|
self._clean_returned_data(data['ansible_facts'])
|
||||||
|
data['ansible_facts'] = wrap_var(data['ansible_facts'])
|
||||||
|
if 'add_host' in data and isinstance(data['add_host'].get('host_vars', None), dict):
|
||||||
|
self._clean_returned_data(data['add_host']['host_vars'])
|
||||||
|
data['add_host'] = wrap_var(data['add_host'])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# not valid json, lets try to capture error
|
# not valid json, lets try to capture error
|
||||||
data = dict(failed=True, _ansible_parsed=False)
|
data = dict(failed=True, _ansible_parsed=False)
|
||||||
|
|
|
@ -23,6 +23,7 @@ import pwd
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ansible import constants as C
|
from ansible import constants as C
|
||||||
|
from ansible.compat.six import string_types
|
||||||
from ansible.errors import AnsibleError
|
from ansible.errors import AnsibleError
|
||||||
from ansible.module_utils._text import to_bytes, to_native, to_text
|
from ansible.module_utils._text import to_bytes, to_native, to_text
|
||||||
from ansible.module_utils.pycompat24 import get_exception
|
from ansible.module_utils.pycompat24 import get_exception
|
||||||
|
|
|
@ -30,8 +30,9 @@ from numbers import Number
|
||||||
from jinja2 import Environment
|
from jinja2 import Environment
|
||||||
from jinja2.loaders import FileSystemLoader
|
from jinja2.loaders import FileSystemLoader
|
||||||
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
|
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
|
||||||
|
from jinja2.nodes import EvalContext
|
||||||
from jinja2.utils import concat as j2_concat
|
from jinja2.utils import concat as j2_concat
|
||||||
from jinja2.runtime import StrictUndefined
|
from jinja2.runtime import Context, StrictUndefined
|
||||||
|
|
||||||
from ansible import constants as C
|
from ansible import constants as C
|
||||||
from ansible.compat.six import string_types, text_type
|
from ansible.compat.six import string_types, text_type
|
||||||
|
@ -41,7 +42,7 @@ from ansible.template.safe_eval import safe_eval
|
||||||
from ansible.template.template import AnsibleJ2Template
|
from ansible.template.template import AnsibleJ2Template
|
||||||
from ansible.template.vars import AnsibleJ2Vars
|
from ansible.template.vars import AnsibleJ2Vars
|
||||||
from ansible.module_utils._text import to_native, to_text
|
from ansible.module_utils._text import to_native, to_text
|
||||||
|
from ansible.vars.unsafe_proxy import UnsafeProxy, wrap_var
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
|
@ -126,6 +127,62 @@ def _count_newlines_from_end(in_str):
|
||||||
# Uncommon cases: zero length string and string containing only newlines
|
# Uncommon cases: zero length string and string containing only newlines
|
||||||
return i
|
return i
|
||||||
|
|
||||||
|
class AnsibleEvalContext(EvalContext):
|
||||||
|
'''
|
||||||
|
A custom jinja2 EvalContext, which is currently unused and saved
|
||||||
|
here for possible future use.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AnsibleContext(Context):
|
||||||
|
'''
|
||||||
|
A custom context, which intercepts resolve() calls and sets a flag
|
||||||
|
internally if any variable lookup returns an AnsibleUnsafe value. This
|
||||||
|
flag is checked post-templating, and (when set) will result in the
|
||||||
|
final templated result being wrapped via UnsafeProxy.
|
||||||
|
'''
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(AnsibleContext, self).__init__(*args, **kwargs)
|
||||||
|
self.eval_ctx = AnsibleEvalContext(self.environment, self.name)
|
||||||
|
self.unsafe = False
|
||||||
|
|
||||||
|
def _is_unsafe(self, val):
|
||||||
|
'''
|
||||||
|
Our helper function, which will also recursively check dict and
|
||||||
|
list entries due to the fact that they may be repr'd and contain
|
||||||
|
a key or value which contains jinja2 syntax and would otherwise
|
||||||
|
lose the AnsibleUnsafe value.
|
||||||
|
'''
|
||||||
|
if isinstance(val, dict):
|
||||||
|
for key in val.keys():
|
||||||
|
if self._is_unsafe(val[key]):
|
||||||
|
return True
|
||||||
|
elif isinstance(val, list):
|
||||||
|
for item in val:
|
||||||
|
if self._is_unsafe(item):
|
||||||
|
return True
|
||||||
|
elif isinstance(val, string_types) and hasattr(val, '__UNSAFE__'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def resolve(self, key):
|
||||||
|
'''
|
||||||
|
The intercepted resolve(), which uses the helper above to set the
|
||||||
|
internal flag whenever an unsafe variable value is returned.
|
||||||
|
'''
|
||||||
|
val = super(AnsibleContext, self).resolve(key)
|
||||||
|
if val is not None and not self.unsafe:
|
||||||
|
if self._is_unsafe(val):
|
||||||
|
self.unsafe = True
|
||||||
|
return val
|
||||||
|
|
||||||
|
class AnsibleEnvironment(Environment):
|
||||||
|
'''
|
||||||
|
Our custom environment, which simply allows us to override the class-level
|
||||||
|
values for the Template and Context classes used by jinja2 internally.
|
||||||
|
'''
|
||||||
|
context_class = AnsibleContext
|
||||||
|
template_class = AnsibleJ2Template
|
||||||
|
|
||||||
class Templar:
|
class Templar:
|
||||||
'''
|
'''
|
||||||
|
@ -159,14 +216,13 @@ class Templar:
|
||||||
self._fail_on_filter_errors = True
|
self._fail_on_filter_errors = True
|
||||||
self._fail_on_undefined_errors = C.DEFAULT_UNDEFINED_VAR_BEHAVIOR
|
self._fail_on_undefined_errors = C.DEFAULT_UNDEFINED_VAR_BEHAVIOR
|
||||||
|
|
||||||
self.environment = Environment(
|
self.environment = AnsibleEnvironment(
|
||||||
trim_blocks=True,
|
trim_blocks=True,
|
||||||
undefined=StrictUndefined,
|
undefined=StrictUndefined,
|
||||||
extensions=self._get_extensions(),
|
extensions=self._get_extensions(),
|
||||||
finalize=self._finalize,
|
finalize=self._finalize,
|
||||||
loader=FileSystemLoader(self._basedir),
|
loader=FileSystemLoader(self._basedir),
|
||||||
)
|
)
|
||||||
self.environment.template_class = AnsibleJ2Template
|
|
||||||
|
|
||||||
self.SINGLE_VAR = re.compile(r"^%s\s*(\w*)\s*%s$" % (self.environment.variable_start_string, self.environment.variable_end_string))
|
self.SINGLE_VAR = re.compile(r"^%s\s*(\w*)\s*%s$" % (self.environment.variable_start_string, self.environment.variable_end_string))
|
||||||
|
|
||||||
|
@ -229,7 +285,7 @@ class Templar:
|
||||||
def _clean_data(self, orig_data):
|
def _clean_data(self, orig_data):
|
||||||
''' remove jinja2 template tags from a string '''
|
''' remove jinja2 template tags from a string '''
|
||||||
|
|
||||||
if not isinstance(orig_data, string_types) or hasattr(orig_data, '__ENCRYPTED__'):
|
if not isinstance(orig_data, string_types) or hasattr(orig_data, '__ENCRYPTED__') or hasattr(orig_data, '__UNSAFE__'):
|
||||||
return orig_data
|
return orig_data
|
||||||
|
|
||||||
with contextlib.closing(StringIO(orig_data)) as data:
|
with contextlib.closing(StringIO(orig_data)) as data:
|
||||||
|
@ -292,11 +348,12 @@ class Templar:
|
||||||
# Don't template unsafe variables, instead drop them back down to their constituent type.
|
# Don't template unsafe variables, instead drop them back down to their constituent type.
|
||||||
if hasattr(variable, '__UNSAFE__'):
|
if hasattr(variable, '__UNSAFE__'):
|
||||||
if isinstance(variable, text_type):
|
if isinstance(variable, text_type):
|
||||||
return self._clean_data(variable)
|
rval = self._clean_data(variable)
|
||||||
else:
|
else:
|
||||||
# Do we need to convert these into text_type as well?
|
# Do we need to convert these into text_type as well?
|
||||||
# return self._clean_data(to_text(variable._obj, nonstring='passthru'))
|
# return self._clean_data(to_text(variable._obj, nonstring='passthru'))
|
||||||
return self._clean_data(variable._obj)
|
rval = self._clean_data(variable._obj)
|
||||||
|
return rval
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if convert_bare:
|
if convert_bare:
|
||||||
|
@ -328,14 +385,23 @@ class Templar:
|
||||||
if cache and sha1_hash in self._cached_result:
|
if cache and sha1_hash in self._cached_result:
|
||||||
result = self._cached_result[sha1_hash]
|
result = self._cached_result[sha1_hash]
|
||||||
else:
|
else:
|
||||||
result = self.do_template(variable, preserve_trailing_newlines=preserve_trailing_newlines, escape_backslashes=escape_backslashes, fail_on_undefined=fail_on_undefined, overrides=overrides)
|
result = self.do_template(
|
||||||
|
variable,
|
||||||
|
preserve_trailing_newlines=preserve_trailing_newlines,
|
||||||
|
escape_backslashes=escape_backslashes,
|
||||||
|
fail_on_undefined=fail_on_undefined,
|
||||||
|
overrides=overrides,
|
||||||
|
)
|
||||||
if convert_data and not self._no_type_regex.match(variable):
|
if convert_data and not self._no_type_regex.match(variable):
|
||||||
# if this looks like a dictionary or list, convert it to such using the safe_eval method
|
# if this looks like a dictionary or list, convert it to such using the safe_eval method
|
||||||
if (result.startswith("{") and not result.startswith(self.environment.variable_start_string)) or \
|
if (result.startswith("{") and not result.startswith(self.environment.variable_start_string)) or \
|
||||||
result.startswith("[") or result in ("True", "False"):
|
result.startswith("[") or result in ("True", "False"):
|
||||||
|
unsafe = hasattr(result, '__UNSAFE__')
|
||||||
eval_results = safe_eval(result, locals=self._available_variables, include_exceptions=True)
|
eval_results = safe_eval(result, locals=self._available_variables, include_exceptions=True)
|
||||||
if eval_results[1] is None:
|
if eval_results[1] is None:
|
||||||
result = eval_results[0]
|
result = eval_results[0]
|
||||||
|
if unsafe:
|
||||||
|
result = wrap_var(result)
|
||||||
else:
|
else:
|
||||||
# FIXME: if the safe_eval raised an error, should we do something with it?
|
# FIXME: if the safe_eval raised an error, should we do something with it?
|
||||||
pass
|
pass
|
||||||
|
@ -435,7 +501,6 @@ class Templar:
|
||||||
ran = None
|
ran = None
|
||||||
|
|
||||||
if ran:
|
if ran:
|
||||||
from ansible.vars.unsafe_proxy import UnsafeProxy, wrap_var
|
|
||||||
if wantlist:
|
if wantlist:
|
||||||
ran = wrap_var(ran)
|
ran = wrap_var(ran)
|
||||||
else:
|
else:
|
||||||
|
@ -505,6 +570,8 @@ class Templar:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = j2_concat(rf)
|
res = j2_concat(rf)
|
||||||
|
if new_context.unsafe:
|
||||||
|
res = wrap_var(res)
|
||||||
except TypeError as te:
|
except TypeError as te:
|
||||||
if 'StrictUndefined' in to_native(te):
|
if 'StrictUndefined' in to_native(te):
|
||||||
errmsg = "Unable to look up a name or access an attribute in template string (%s).\n" % to_native(data)
|
errmsg = "Unable to look up a name or access an attribute in template string (%s).\n" % to_native(data)
|
||||||
|
|
|
@ -33,5 +33,5 @@ class AnsibleJ2Template(jinja2.environment.Template):
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def new_context(self, vars=None, shared=False, locals=None):
|
def new_context(self, vars=None, shared=False, locals=None):
|
||||||
return jinja2.runtime.Context(self.environment, vars.add_locals(locals), self.name, self.blocks)
|
return self.environment.context_class(self.environment, vars.add_locals(locals), self.name, self.blocks)
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ class AnsibleJ2Vars:
|
||||||
# HostVars is special, return it as-is, as is the special variable
|
# HostVars is special, return it as-is, as is the special variable
|
||||||
# 'vars', which contains the vars structure
|
# 'vars', which contains the vars structure
|
||||||
from ansible.vars.hostvars import HostVars
|
from ansible.vars.hostvars import HostVars
|
||||||
if isinstance(variable, dict) and varname == "vars" or isinstance(variable, HostVars):
|
if isinstance(variable, dict) and varname == "vars" or isinstance(variable, HostVars) or hasattr(variable, '__UNSAFE__'):
|
||||||
return variable
|
return variable
|
||||||
else:
|
else:
|
||||||
value = None
|
value = None
|
||||||
|
|
|
@ -64,7 +64,6 @@ __all__ = ['UnsafeProxy', 'AnsibleUnsafe', 'AnsibleJSONUnsafeEncoder', 'AnsibleJ
|
||||||
class AnsibleUnsafe(object):
|
class AnsibleUnsafe(object):
|
||||||
__UNSAFE__ = True
|
__UNSAFE__ = True
|
||||||
|
|
||||||
|
|
||||||
class AnsibleUnsafeText(text_type, AnsibleUnsafe):
|
class AnsibleUnsafeText(text_type, AnsibleUnsafe):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -101,7 +100,7 @@ class AnsibleJSONUnsafeDecoder(json.JSONDecoder):
|
||||||
def _wrap_dict(v):
|
def _wrap_dict(v):
|
||||||
for k in v.keys():
|
for k in v.keys():
|
||||||
if v[k] is not None:
|
if v[k] is not None:
|
||||||
v[k] = wrap_var(v[k])
|
v[wrap_var(k)] = wrap_var(v[k])
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue