From owner-svn-ports-all@freebsd.org Thu Jan 12 23:11:17 2017 Return-Path: Delivered-To: svn-ports-all@mailman.ysv.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2001:1900:2254:206a::19:1]) by mailman.ysv.freebsd.org (Postfix) with ESMTP id 40394CAC3A5; Thu, 12 Jan 2017 23:11:17 +0000 (UTC) (envelope-from lifanov@FreeBSD.org) Received: from repo.freebsd.org (repo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:0]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (Client did not present a certificate) by mx1.freebsd.org (Postfix) with ESMTPS id 0900E172A; Thu, 12 Jan 2017 23:11:16 +0000 (UTC) (envelope-from lifanov@FreeBSD.org) Received: from repo.freebsd.org ([127.0.1.37]) by repo.freebsd.org (8.15.2/8.15.2) with ESMTP id v0CNBGsZ017385; Thu, 12 Jan 2017 23:11:16 GMT (envelope-from lifanov@FreeBSD.org) Received: (from lifanov@localhost) by repo.freebsd.org (8.15.2/8.15.2/Submit) id v0CNBFqA017382; Thu, 12 Jan 2017 23:11:15 GMT (envelope-from lifanov@FreeBSD.org) Message-Id: <201701122311.v0CNBFqA017382@repo.freebsd.org> X-Authentication-Warning: repo.freebsd.org: lifanov set sender to lifanov@FreeBSD.org using -f From: Nikolai Lifanov Date: Thu, 12 Jan 2017 23:11:15 +0000 (UTC) To: ports-committers@freebsd.org, svn-ports-all@freebsd.org, svn-ports-head@freebsd.org Subject: svn commit: r431329 - in head/sysutils/ansible: . files X-SVN-Group: ports-head MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-BeenThere: svn-ports-all@freebsd.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: SVN commit messages for the ports tree List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Thu, 12 Jan 2017 23:11:17 -0000 Author: lifanov Date: Thu Jan 12 23:11:15 2017 New Revision: 431329 URL: https://svnweb.freebsd.org/changeset/ports/431329 Log: sysutils/ansible: address CVE-2016-9587 Reviewed by: matthew Approved by: matthew (mentor) MFH: 2017Q1 Security: CVE-2016-9587 Security: https://vuxml.FreeBSD.org/freebsd/a93c3287-d8fd-11e6-be5c-001fbc0f280f.html Differential Revision: https://reviews.freebsd.org/D9158 Added: head/sysutils/ansible/files/extra-patch-cc4634a (contents, props changed) head/sysutils/ansible/files/extra-patch-eb8c26c (contents, props changed) head/sysutils/ansible/files/extra-patch-ec84ff6 (contents, props changed) Modified: head/sysutils/ansible/Makefile Modified: head/sysutils/ansible/Makefile ============================================================================== --- head/sysutils/ansible/Makefile Thu Jan 12 23:09:56 2017 (r431328) +++ head/sysutils/ansible/Makefile Thu Jan 12 23:11:15 2017 (r431329) @@ -3,7 +3,7 @@ PORTNAME= ansible PORTVERSION?= 2.2.0.0 -PORTREVISION?= 1 +PORTREVISION?= 2 CATEGORIES= sysutils python MASTER_SITES= http://releases.ansible.com/ansible/ @@ -18,7 +18,10 @@ RUN_DEPENDS?= ${PYTHON_PKGNAMEPREFIX}yam ${PYTHON_PKGNAMEPREFIX}paramiko>0:security/py-paramiko \ ${PYTHON_PKGNAMEPREFIX}Jinja2>0:devel/py-Jinja2 -EXTRA_PATCHES?= ${FILESDIR}/extra-patch-872594b +EXTRA_PATCHES?= ${FILESDIR}/extra-patch-872594b \ + ${FILESDIR}/extra-patch-ec84ff6 \ + ${FILESDIR}/extra-patch-eb8c26c \ + ${FILESDIR}/extra-patch-cc4634a NO_ARCH= yes USES?= cpe python shebangfix Added: head/sysutils/ansible/files/extra-patch-cc4634a ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ head/sysutils/ansible/files/extra-patch-cc4634a Thu Jan 12 23:11:15 2017 (r431329) @@ -0,0 +1,159 @@ +From cc4634a5e73c06c6b4581f11171289ca9228391e Mon Sep 17 00:00:00 2001 +From: James Cammarata +Date: Tue, 10 Jan 2017 16:54:00 -0600 +Subject: [PATCH] Additional fixes for security related to CVE-2016-9587 + +(cherry picked from commit d316068831f9e08ef96833200ec7df2132263966) +--- + lib/ansible/playbook/conditional.py | 10 +++++----- + lib/ansible/template/__init__.py | 28 ++++++++++++++-------------- + 2 files changed, 19 insertions(+), 19 deletions(-) + +diff --git lib/ansible/playbook/conditional.py lib/ansible/playbook/conditional.py +index 99e377c..57e20a0 100644 +--- lib/ansible/playbook/conditional.py ++++ lib/ansible/playbook/conditional.py +@@ -104,7 +104,7 @@ def _check_conditional(self, conditional, templar, all_vars): + if conditional is None or conditional == '': + return True + +- if conditional in all_vars and '-' not in text_type(all_vars[conditional]): ++ if conditional in all_vars and re.match("^[_A-Za-z][_a-zA-Z0-9]*$", conditional): + conditional = all_vars[conditional] + + # make sure the templar is using the variables specified with this method +@@ -116,12 +116,12 @@ def _check_conditional(self, conditional, templar, all_vars): + return conditional + + # 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) ++ disable_lookups = False ++ if hasattr(conditional, '__UNSAFE__'): ++ disable_lookups = True + + presented = "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional +- val = templar.template(presented).strip() ++ val = templar.template(presented, disable_lookups=disable_lookups).strip() + if val == "True": + return True + elif val == "False": +diff --git lib/ansible/template/__init__.py lib/ansible/template/__init__.py +index 53b2675..1a43486 100644 +--- lib/ansible/template/__init__.py ++++ lib/ansible/template/__init__.py +@@ -30,10 +30,8 @@ + from jinja2 import Environment + from jinja2.loaders import FileSystemLoader + from jinja2.exceptions import TemplateSyntaxError, UndefinedError +-from jinja2.nodes import EvalContext + from jinja2.utils import concat as j2_concat + from jinja2.runtime import Context, StrictUndefined +- + from ansible import constants as C + from ansible.compat.six import string_types, text_type + from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleUndefinedVariable +@@ -42,7 +40,6 @@ + from ansible.template.template import AnsibleJ2Template + from ansible.template.vars import AnsibleJ2Vars + from ansible.module_utils._text import to_native, to_text +-from ansible.vars.unsafe_proxy import UnsafeProxy, wrap_var + + try: + from hashlib import sha1 +@@ -127,13 +124,6 @@ def _count_newlines_from_end(in_str): + # Uncommon cases: zero length string and string containing only newlines + 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 +@@ -143,7 +133,6 @@ class AnsibleContext(Context): + ''' + 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): +@@ -335,7 +324,7 @@ def set_available_variables(self, variables): + self._available_variables = variables + self._cached_result = {} + +- def template(self, variable, convert_bare=False, preserve_trailing_newlines=True, escape_backslashes=True, fail_on_undefined=None, overrides=None, convert_data=True, static_vars = [''], cache = True, bare_deprecated=True): ++ def template(self, variable, convert_bare=False, preserve_trailing_newlines=True, escape_backslashes=True, fail_on_undefined=None, overrides=None, convert_data=True, static_vars = [''], cache = True, bare_deprecated=True, disable_lookups=False): + ''' + Templates (possibly recursively) any given data as input. If convert_bare is + set to True, the given data will be wrapped as a jinja2 variable ('{{foo}}') +@@ -391,6 +380,7 @@ def template(self, variable, convert_bare=False, preserve_trailing_newlines=True + escape_backslashes=escape_backslashes, + fail_on_undefined=fail_on_undefined, + overrides=overrides, ++ disable_lookups=disable_lookups, + ) + unsafe = hasattr(result, '__UNSAFE__') + if convert_data and not self._no_type_regex.match(variable) and not unsafe: +@@ -401,6 +391,7 @@ def template(self, variable, convert_bare=False, preserve_trailing_newlines=True + if eval_results[1] is None: + result = eval_results[0] + if unsafe: ++ from ansible.vars.unsafe_proxy import wrap_var + result = wrap_var(result) + else: + # FIXME: if the safe_eval raised an error, should we do something with it? +@@ -482,6 +473,9 @@ def _finalize(self, thing): + ''' + return thing if thing is not None else '' + ++ def _fail_lookup(self, name, *args, **kwargs): ++ raise AnsibleError("The lookup `%s` was found, however lookups were disabled from templating" % name) ++ + def _lookup(self, name, *args, **kwargs): + instance = self._lookup_loader.get(name.lower(), loader=self._loader, templar=self) + +@@ -501,6 +495,7 @@ def _lookup(self, name, *args, **kwargs): + ran = None + + if ran: ++ from ansible.vars.unsafe_proxy import UnsafeProxy, wrap_var + if wantlist: + ran = wrap_var(ran) + else: +@@ -516,7 +511,7 @@ def _lookup(self, name, *args, **kwargs): + else: + raise AnsibleError("lookup plugin (%s) not found" % name) + +- def _do_template(self, data, preserve_trailing_newlines=True, escape_backslashes=True, fail_on_undefined=None, overrides=None): ++ def _do_template(self, data, preserve_trailing_newlines=True, escape_backslashes=True, fail_on_undefined=None, overrides=None, disable_lookups=False): + # For preserving the number of input newlines in the output (used + # later in this method) + data_newlines = _count_newlines_from_end(data) +@@ -560,7 +555,11 @@ def do_template(self, data, preserve_trailing_newlines=True, escape_backslashes= + else: + return data + +- t.globals['lookup'] = self._lookup ++ if disable_lookups: ++ t.globals['lookup'] = self._fail_lookup ++ else: ++ t.globals['lookup'] = self._lookup ++ + t.globals['finalize'] = self._finalize + + jvars = AnsibleJ2Vars(self, t.globals) +@@ -571,6 +570,7 @@ def do_template(self, data, preserve_trailing_newlines=True, escape_backslashes= + try: + res = j2_concat(rf) + if new_context.unsafe: ++ from ansible.vars.unsafe_proxy import wrap_var + res = wrap_var(res) + except TypeError as te: + if 'StrictUndefined' in to_native(te): Added: head/sysutils/ansible/files/extra-patch-eb8c26c ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ head/sysutils/ansible/files/extra-patch-eb8c26c Thu Jan 12 23:11:15 2017 (r431329) @@ -0,0 +1,60 @@ +From eb8c26c105e8457b86324b64a13fac37d8862d47 Mon Sep 17 00:00:00 2001 +From: Computest +Date: Tue, 10 Jan 2017 16:51:40 -0600 +Subject: [PATCH] Fixing another corner case for security related to + CVE-2016-9587 + +(cherry picked from commit bcceada5d9b78ad77069c78226f8e9b336ff8949) +--- + lib/ansible/template/__init__.py | 6 +++--- + lib/ansible/vars/unsafe_proxy.py | 8 ++++++-- + 2 files changed, 9 insertions(+), 5 deletions(-) + +diff --git lib/ansible/template/__init__.py lib/ansible/template/__init__.py +index 4e24fbe..53b2675 100644 +--- lib/ansible/template/__init__.py ++++ lib/ansible/template/__init__.py +@@ -155,7 +155,7 @@ def _is_unsafe(self, val): + ''' + if isinstance(val, dict): + for key in val.keys(): +- if self._is_unsafe(val[key]): ++ if self._is_unsafe(key) or self._is_unsafe(val[key]): + return True + elif isinstance(val, list): + for item in val: +@@ -392,11 +392,11 @@ def template(self, variable, convert_bare=False, preserve_trailing_newlines=True + fail_on_undefined=fail_on_undefined, + overrides=overrides, + ) +- if convert_data and not self._no_type_regex.match(variable): ++ unsafe = hasattr(result, '__UNSAFE__') ++ if convert_data and not self._no_type_regex.match(variable) and not unsafe: + # 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 \ + result.startswith("[") or result in ("True", "False"): +- unsafe = hasattr(result, '__UNSAFE__') + eval_results = safe_eval(result, locals=self._available_variables, include_exceptions=True) + if eval_results[1] is None: + result = eval_results[0] +diff --git lib/ansible/vars/unsafe_proxy.py lib/ansible/vars/unsafe_proxy.py +index 426410a..4284705 100644 +--- lib/ansible/vars/unsafe_proxy.py ++++ lib/ansible/vars/unsafe_proxy.py +@@ -98,10 +98,14 @@ def decode(self, obj): + + + def _wrap_dict(v): ++ # Create new dict to get rid of the keys that are not wrapped. ++ new = {} + for k in v.keys(): + if v[k] is not None: +- v[wrap_var(k)] = wrap_var(v[k]) +- return v ++ new[wrap_var(k)] = wrap_var(v[k]) ++ else: ++ new[wrap_var(k)] = None ++ return new + + + def _wrap_list(v): Added: head/sysutils/ansible/files/extra-patch-ec84ff6 ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ head/sysutils/ansible/files/extra-patch-ec84ff6 Thu Jan 12 23:11:15 2017 (r431329) @@ -0,0 +1,359 @@ +--- lib/ansible/playbook/conditional.py.orig 2016-11-01 03:43:19 UTC ++++ lib/ansible/playbook/conditional.py +@@ -19,6 +19,8 @@ + from __future__ import (absolute_import, division, print_function) + __metaclass__ = type + ++import re ++ + from jinja2.exceptions import UndefinedError + + from ansible.compat.six import text_type +@@ -26,6 +28,9 @@ from ansible.errors import AnsibleError, + from ansible.playbook.attribute import FieldAttribute + from ansible.template import Templar + from ansible.module_utils._text import to_native ++from ansible.vars.unsafe_proxy import wrap_var ++ ++LOOKUP_REGEX = re.compile(r'lookup\s*\(') + + class Conditional: + +@@ -100,9 +105,12 @@ class Conditional: + return conditional + + # 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 +- conditional = templar.template(presented) +- val = conditional.strip() ++ val = templar.template(presented).strip() + if val == "True": + return True + elif val == "False": +--- lib/ansible/plugins/action/__init__.py.orig 2016-11-01 03:43:19 UTC ++++ lib/ansible/plugins/action/__init__.py +@@ -30,9 +30,8 @@ import tempfile + import time + from abc import ABCMeta, abstractmethod + +-from ansible.compat.six import binary_type, text_type, iteritems, with_metaclass +- + from ansible import constants as C ++from ansible.compat.six import binary_type, string_types, text_type, iteritems, with_metaclass + from ansible.errors import AnsibleError, AnsibleConnectionFailure + from ansible.executor.module_common import modify_module + from ansible.module_utils._text import to_bytes, to_native, to_text +@@ -40,6 +39,7 @@ from ansible.module_utils.json_utils imp + from ansible.parsing.utils.jsonify import jsonify + from ansible.playbook.play_context import MAGIC_VARIABLE_MAPPING + from ansible.release import __version__ ++from ansible.vars.unsafe_proxy import wrap_var + + + try: +@@ -449,6 +449,8 @@ class ActionBase(with_metaclass(ABCMeta, + # happens sometimes when it is a dir and not on bsd + if 'checksum' not in mystat['stat']: + 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'] + +@@ -664,6 +666,39 @@ class ActionBase(with_metaclass(ABCMeta, + display.debug("done with _execute_module (%s, %s)" % (module_name, module_args)) + return data + ++ def _clean_returned_data(self, data): ++ remove_keys = set() ++ fact_keys = set(data.keys()) ++ # first we add all of our magic variable names to the set of ++ # keys we want to remove from facts ++ for magic_var in MAGIC_VARIABLE_MAPPING: ++ remove_keys.update(fact_keys.intersection(MAGIC_VARIABLE_MAPPING[magic_var])) ++ # next we remove any connection plugin specific vars ++ for conn_path in self._shared_loader_obj.connection_loader.all(path_only=True): ++ try: ++ conn_name = os.path.splitext(os.path.basename(conn_path))[0] ++ re_key = re.compile('^ansible_%s_' % conn_name) ++ for fact_key in fact_keys: ++ if re_key.match(fact_key): ++ remove_keys.add(fact_key) ++ except AttributeError: ++ pass ++ ++ # remove some KNOWN keys ++ for hard in ['ansible_rsync_path', 'ansible_playbook_python']: ++ if hard in fact_keys: ++ remove_keys.add(hard) ++ ++ # finally, we search for interpreter keys to remove ++ re_interp = re.compile('^ansible_.*_interpreter$') ++ for fact_key in fact_keys: ++ if re_interp.match(fact_key): ++ remove_keys.add(fact_key) ++ # then we remove them (except for ssh host keys) ++ for r_key in remove_keys: ++ if not r_key.startswith('ansible_ssh_host_key_'): ++ del data[r_key] ++ + def _parse_returned_data(self, res): + try: + filtered_output, warnings = _filter_non_json_lines(res.get('stdout', u'')) +@@ -672,31 +707,11 @@ class ActionBase(with_metaclass(ABCMeta, + data = json.loads(filtered_output) + data['_ansible_parsed'] = True + if 'ansible_facts' in data and isinstance(data['ansible_facts'], dict): +- remove_keys = set() +- fact_keys = set(data['ansible_facts'].keys()) +- # first we add all of our magic variable names to the set of +- # keys we want to remove from facts +- for magic_var in MAGIC_VARIABLE_MAPPING: +- remove_keys.update(fact_keys.intersection(MAGIC_VARIABLE_MAPPING[magic_var])) +- # next we remove any connection plugin specific vars +- for conn_path in self._shared_loader_obj.connection_loader.all(path_only=True): +- try: +- conn_name = os.path.splitext(os.path.basename(conn_path))[0] +- re_key = re.compile('^ansible_%s_' % conn_name) +- for fact_key in fact_keys: +- if re_key.match(fact_key): +- remove_keys.add(fact_key) +- except AttributeError: +- pass +- # finally, we search for interpreter keys to remove +- re_interp = re.compile('^ansible_.*_interpreter$') +- for fact_key in fact_keys: +- if re_interp.match(fact_key): +- remove_keys.add(fact_key) +- # then we remove them (except for ssh host keys) +- for r_key in remove_keys: +- if not r_key.startswith('ansible_ssh_host_key_'): +- del data['ansible_facts'][r_key] ++ 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: + # not valid json, lets try to capture error + data = dict(failed=True, _ansible_parsed=False) +--- lib/ansible/plugins/action/template.py.orig 2016-11-01 03:43:19 UTC ++++ lib/ansible/plugins/action/template.py +@@ -23,6 +23,7 @@ import pwd + import time + + from ansible import constants as C ++from ansible.compat.six import string_types + from ansible.errors import AnsibleError + from ansible.module_utils._text import to_bytes, to_native, to_text + from ansible.plugins.action import ActionBase +--- lib/ansible/template/__init__.py.orig 2016-11-01 03:43:19 UTC ++++ lib/ansible/template/__init__.py +@@ -30,8 +30,9 @@ from numbers import Number + from jinja2 import Environment + from jinja2.loaders import FileSystemLoader + from jinja2.exceptions import TemplateSyntaxError, UndefinedError ++from jinja2.nodes import EvalContext + 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.compat.six import string_types, text_type +@@ -41,7 +42,7 @@ from ansible.template.safe_eval import s + from ansible.template.template import AnsibleJ2Template + from ansible.template.vars import AnsibleJ2Vars + from ansible.module_utils._text import to_native, to_text +- ++from ansible.vars.unsafe_proxy import UnsafeProxy, wrap_var + + try: + 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 + 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: + ''' +@@ -159,14 +216,13 @@ class Templar: + self._fail_on_filter_errors = True + self._fail_on_undefined_errors = C.DEFAULT_UNDEFINED_VAR_BEHAVIOR + +- self.environment = Environment( ++ self.environment = AnsibleEnvironment( + trim_blocks=True, + undefined=StrictUndefined, + extensions=self._get_extensions(), + finalize=self._finalize, + 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)) + +@@ -229,7 +285,7 @@ class Templar: + def _clean_data(self, orig_data): + ''' 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 + + 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. + if hasattr(variable, '__UNSAFE__'): + if isinstance(variable, text_type): +- return self._clean_data(variable) ++ rval = self._clean_data(variable) + else: + # 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(variable._obj) ++ rval = self._clean_data(variable._obj) ++ return rval + + try: + if convert_bare: +@@ -328,14 +385,23 @@ class Templar: + if cache and sha1_hash in self._cached_result: + result = self._cached_result[sha1_hash] + 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 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 \ + result.startswith("[") or result in ("True", "False"): ++ unsafe = hasattr(result, '__UNSAFE__') + eval_results = safe_eval(result, locals=self._available_variables, include_exceptions=True) + if eval_results[1] is None: + result = eval_results[0] ++ if unsafe: ++ result = wrap_var(result) + else: + # FIXME: if the safe_eval raised an error, should we do something with it? + pass +@@ -435,7 +501,6 @@ class Templar: + ran = None + + if ran: +- from ansible.vars.unsafe_proxy import UnsafeProxy, wrap_var + if wantlist: + ran = wrap_var(ran) + else: +@@ -505,6 +570,8 @@ class Templar: + + try: + res = j2_concat(rf) ++ if new_context.unsafe: ++ res = wrap_var(res) + except TypeError as 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) +--- lib/ansible/template/template.py.orig 2016-11-01 03:43:19 UTC ++++ lib/ansible/template/template.py +@@ -33,5 +33,5 @@ class AnsibleJ2Template(jinja2.environme + ''' + + 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) + +--- lib/ansible/template/vars.py.orig 2016-11-01 03:43:19 UTC ++++ lib/ansible/template/vars.py +@@ -82,7 +82,7 @@ class AnsibleJ2Vars: + # HostVars is special, return it as-is, as is the special variable + # 'vars', which contains the vars structure + 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 + else: + value = None +--- lib/ansible/vars/unsafe_proxy.py.orig 2016-11-01 03:43:19 UTC ++++ lib/ansible/vars/unsafe_proxy.py +@@ -64,7 +64,6 @@ __all__ = ['UnsafeProxy', 'AnsibleUnsafe + class AnsibleUnsafe(object): + __UNSAFE__ = True + +- + class AnsibleUnsafeText(text_type, AnsibleUnsafe): + pass + +@@ -101,7 +100,7 @@ class AnsibleJSONUnsafeDecoder(json.JSON + def _wrap_dict(v): + for k in v.keys(): + if v[k] is not None: +- v[k] = wrap_var(v[k]) ++ v[wrap_var(k)] = wrap_var(v[k]) + return v + +