Skip site navigation (1)Skip section navigation (2)
Date:      Fri, 13 Jan 2017 04:27:58 +0000 (UTC)
From:      Nikolai Lifanov <lifanov@FreeBSD.org>
To:        ports-committers@freebsd.org, svn-ports-all@freebsd.org, svn-ports-branches@freebsd.org
Subject:   svn commit: r431341 - in branches/2017Q1/sysutils/ansible: . files
Message-ID:  <201701130427.v0D4Rw7p047157@repo.freebsd.org>

next in thread | raw e-mail | index | archive | help
Author: lifanov
Date: Fri Jan 13 04:27:58 2017
New Revision: 431341
URL: https://svnweb.freebsd.org/changeset/ports/431341

Log:
  MFH: r431329
  
  sysutils/ansible: address CVE-2016-9587
  
  Reviewed by:	matthew
  Approved by:	matthew (mentor)
  Security:	CVE-2016-9587
  Security:	https://vuxml.FreeBSD.org/freebsd/a93c3287-d8fd-11e6-be5c-001fbc0f280f.html
  Differential Revision:	https://reviews.freebsd.org/D9158
  
  Approved by:	ports-secteam (junovitch)

Added:
  branches/2017Q1/sysutils/ansible/files/extra-patch-cc4634a
     - copied unchanged from r431329, head/sysutils/ansible/files/extra-patch-cc4634a
  branches/2017Q1/sysutils/ansible/files/extra-patch-eb8c26c
     - copied unchanged from r431329, head/sysutils/ansible/files/extra-patch-eb8c26c
  branches/2017Q1/sysutils/ansible/files/extra-patch-ec84ff6
     - copied unchanged from r431329, head/sysutils/ansible/files/extra-patch-ec84ff6
Modified:
  branches/2017Q1/sysutils/ansible/Makefile
Directory Properties:
  branches/2017Q1/   (props changed)

Modified: branches/2017Q1/sysutils/ansible/Makefile
==============================================================================
--- branches/2017Q1/sysutils/ansible/Makefile	Fri Jan 13 03:22:05 2017	(r431340)
+++ branches/2017Q1/sysutils/ansible/Makefile	Fri Jan 13 04:27:58 2017	(r431341)
@@ -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

Copied: branches/2017Q1/sysutils/ansible/files/extra-patch-cc4634a (from r431329, head/sysutils/ansible/files/extra-patch-cc4634a)
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ branches/2017Q1/sysutils/ansible/files/extra-patch-cc4634a	Fri Jan 13 04:27:58 2017	(r431341, copy of r431329, head/sysutils/ansible/files/extra-patch-cc4634a)
@@ -0,0 +1,159 @@
+From cc4634a5e73c06c6b4581f11171289ca9228391e Mon Sep 17 00:00:00 2001
+From: James Cammarata <jimi@sngx.net>
+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):

Copied: branches/2017Q1/sysutils/ansible/files/extra-patch-eb8c26c (from r431329, head/sysutils/ansible/files/extra-patch-eb8c26c)
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ branches/2017Q1/sysutils/ansible/files/extra-patch-eb8c26c	Fri Jan 13 04:27:58 2017	(r431341, copy of r431329, head/sysutils/ansible/files/extra-patch-eb8c26c)
@@ -0,0 +1,60 @@
+From eb8c26c105e8457b86324b64a13fac37d8862d47 Mon Sep 17 00:00:00 2001
+From: Computest <anon@@computest.nl>
+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):

Copied: branches/2017Q1/sysutils/ansible/files/extra-patch-ec84ff6 (from r431329, head/sysutils/ansible/files/extra-patch-ec84ff6)
==============================================================================
--- /dev/null	00:00:00 1970	(empty, because file is newly added)
+++ branches/2017Q1/sysutils/ansible/files/extra-patch-ec84ff6	Fri Jan 13 04:27:58 2017	(r431341, copy of r431329, head/sysutils/ansible/files/extra-patch-ec84ff6)
@@ -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
+ 
+ 



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201701130427.v0D4Rw7p047157>