VYPR
Critical severityNVD Advisory· Published Feb 20, 2020· Updated Aug 6, 2024

CVE-2014-4657

CVE-2014-4657

Description

The safe_eval function in Ansible before 1.5.4 does not properly restrict the code subset, which allows remote attackers to execute arbitrary code via crafted instructions.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Ansible before 1.5.4's safe_eval function allowed arbitrary code execution via crafted instructions due to incomplete restriction of Python AST nodes.

Vulnerability

Analysis

CVE-2014-4657 is a code injection vulnerability in the safe_eval function of Ansible versions prior to 1.5.4. The safe_eval function was designed to safely evaluate Jinja2 template expressions by restricting the allowed Python abstract syntax tree (AST) node types and built-in functions. However, the initial implementation failed to adequately restrict the code subset, permitting the use of dangerous AST node types like ast.Call and ast.Attribute without sufficient validation [1]. This oversight allowed an attacker to construct malicious template expressions that, when evaluated, could execute arbitrary Python code.

Exploitation

The vulnerability can be exploited by an attacker who is able to supply crafted template instructions that are processed by Ansible. This requires the attacker to have the ability to inject Jinja2 template expressions, for example through a playbook variable or a task parameter. The attack does not require authentication to the Ansible control node itself, but relies on the victim executing a playbook that processes the malicious input [2]. The official fix, implemented in commit 998793fd0ab55705d57527a38cee5e83f535974c [3], strengthened the whitelist of allowed AST nodes and added a blacklist of inherently dangerous built-in function names known as INVALID_CALLS.

Impact

Successful exploitation enables a remote attacker to execute arbitrary Python code on the Ansible control node with the privileges of the Ansible process [2]. This could lead to full compromise of the automation server and subsequent lateral movement to managed hosts, as Ansible typically runs with elevated permissions to manage infrastructure.

Mitigation

The vulnerability is patched in Ansible version 1.5.4 and later [1]. Users are strongly advised to upgrade to at least this version. No workarounds are documented, but limiting exposure by restricting who can supply playbooks and variables reduces risk. This CVE is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
ansiblePyPI
< 1.5.41.5.4

Affected products

2

Patches

1
998793fd0ab5

Fixes to safe_eval

https://github.com/ansible/ansibleJames CammarataMar 31, 2014via ghsa
1 file changed · +72 27
  • lib/ansible/utils/__init__.py+72 27 modified
    @@ -29,6 +29,7 @@
     from ansible.utils import template
     from ansible.callbacks import display
     import ansible.constants as C
    +import ast
     import time
     import StringIO
     import stat
    @@ -945,51 +946,95 @@ def is_list_of_strings(items):
                 return False
         return True
     
    -def safe_eval(str, locals=None, include_exceptions=False):
    +def safe_eval(expr, locals={}, include_exceptions=False):
         '''
         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)
    +
    +    Based on:
    +    http://stackoverflow.com/questions/12523516/using-ast-and-whitelists-to-make-pythons-eval-safe
         '''
    -    # FIXME: is there a more native way to do this?
     
    -    def is_set(var):
    -        return not var.startswith("$") and not '{{' in var
    +    # this is the whitelist of AST nodes we are going to 
    +    # allow in the evaluation. Any node type other than 
    +    # those listed here will raise an exception in our custom
    +    # visitor class defined below.
    +    SAFE_NODES = set(
    +        (
    +            ast.Expression,
    +            ast.Compare,
    +            ast.Str,
    +            ast.List,
    +            ast.Tuple,
    +            ast.Dict,
    +            ast.Call,
    +            ast.Load,
    +            ast.BinOp,
    +            ast.UnaryOp,
    +            ast.Num,
    +            ast.Name,
    +            ast.Add,
    +            ast.Sub,
    +            ast.Mult,
    +            ast.Div,
    +        )
    +    )
    +
    +    # AST node types were expanded after 2.6
    +    if not sys.version.startswith('2.6'):
    +        SAFE_NODES.union(
    +            set(
    +                (ast.Set,)
    +            )
    +        )
     
    -    def is_unset(var):
    -        return var.startswith("$") or '{{' in var
    +    # builtin functions that are not safe to call
    +    INVALID_CALLS = (
    +       'classmethod', 'compile', 'delattr', 'eval', 'execfile', 'file',
    +       'filter', 'help', 'input', 'object', 'open', 'raw_input', 'reduce',
    +       'reload', 'repr', 'setattr', 'staticmethod', 'super', 'type',
    +    )
     
    -    # do not allow method calls to modules
    -    if not isinstance(str, basestring):
    +    class CleansingNodeVisitor(ast.NodeVisitor):
    +        def generic_visit(self, node):
    +            if type(node) not in SAFE_NODES:
    +                #raise Exception("invalid expression (%s) type=%s" % (expr, type(node)))
    +                raise Exception("invalid expression (%s)" % expr)
    +            super(CleansingNodeVisitor, self).generic_visit(node)
    +        def visit_Call(self, call):
    +            if call.func.id in INVALID_CALLS:
    +                raise Exception("invalid function: %s" % call.func.id)
    +
    +    if not isinstance(expr, basestring):
             # already templated to a datastructure, perhaps?
             if include_exceptions:
    -            return (str, None)
    -        return str
    -    if re.search(r'\w\.\w+\(', str):
    -        if include_exceptions:
    -            return (str, None)
    -        return str
    -    # do not allow imports
    -    if re.search(r'import \w+', str):
    -        if include_exceptions:
    -            return (str, None)
    -        return str
    +            return (expr, None)
    +        return expr
    +
         try:
    -        result = None
    -        if not locals:
    -            result = eval(str)
    -        else:
    -            result = eval(str, None, locals)
    +        parsed_tree = ast.parse(expr, mode='eval')
    +        cnv = CleansingNodeVisitor()
    +        cnv.visit(parsed_tree)
    +        compiled = compile(parsed_tree, expr, 'eval')
    +        result = eval(compiled, {}, locals)
    +
             if include_exceptions:
                 return (result, None)
             else:
                 return result
    +    except SyntaxError, e:
    +        # special handling for syntax errors, we just return
    +        # the expression string back as-is
    +        if include_exceptions:
    +            return (expr, None)
    +        return expr
         except Exception, e:
             if include_exceptions:
    -            return (str, e)
    -        return str
    +            return (expr, e)
    +        return expr
     
     
     def listify_lookup_plugin_terms(terms, basedir, inject):
    @@ -1001,7 +1046,7 @@ def listify_lookup_plugin_terms(terms, basedir, inject):
             #    with_items: {{ alist }}
     
             stripped = terms.strip()
    -        if not (stripped.startswith('{') or stripped.startswith('[')) and not stripped.startswith("/"):
    +        if not (stripped.startswith('{') or stripped.startswith('[')) and not stripped.startswith("/") and not stripped.startswith('set(['):
                 # if not already a list, get ready to evaluate with Jinja2
                 # not sure why the "/" is in above code :)
                 try:
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.