CVE-2014-3498: Improper Input Validation (CWE-20)
value: The user module in ansible before 1.6.6 allows remote authenticated users to execute arbitrary commands.
CWE description: The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
CVSS v3 base score: 8.8
Published: 2017-06-08T18:29Z
Repository: https://github.com/ansible/ansible
Language: Python
Files changed: lib/ansible/inventory/script.py, lib/ansible/runner/__init__.py, lib/ansible/runner/return_data.py, lib/ansible/utils/__init__.py, lib/ansible/utils/template.py
Commit date: 2014-07-01 14:14:14 -0500
Commit message: When parsing json from untrusted sources, remove templating tags

Diff: diff --git a/lib/ansible/inventory/script.py b/lib/ansible/inventory/script.py
index 05348b683..aec52256f 100644
--- a/lib/ansible/inventory/script.py
+++ b/lib/ansible/inventory/script.py
@@ -47,11 +47,11 @@ class InventoryScript(object):
         self.groups = self._parse(stderr)
 
     def _parse(self, err):
 
         all_hosts = {}
-        self.raw  = utils.parse_json(self.data)
+        self.raw  = utils.parse_json(self.data, from_remote=True)
         all       = Group('all')
         groups    = dict(all=all)
         group     = None
 
 
diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py
index f9980f857..e810885e3 100644
--- a/lib/ansible/runner/__init__.py
+++ b/lib/ansible/runner/__init__.py
@@ -495,11 +495,11 @@ class Runner(object):
             # not sudoing to root, so maybe can't delete files as that other user
             # have to clean up temp files as original user in a second step
                 cmd2 = conn.shell.remove(tmp, recurse=True)
                 self._low_level_exec_command(conn, cmd2, tmp, sudoable=False)
 
-        data = utils.parse_json(res['stdout'])
+        data = utils.parse_json(res['stdout'], from_remote=True)
         if 'parsed' in data and data['parsed'] == False:
             data['msg'] += res['stderr']
         return ReturnData(conn=conn, result=data)
 
     # *****************************************************
diff --git a/lib/ansible/runner/return_data.py b/lib/ansible/runner/return_data.py
index 8e2252d4a..5b99c4589 100644
--- a/lib/ansible/runner/return_data.py
+++ b/lib/ansible/runner/return_data.py
@@ -41,12 +41,11 @@ class ReturnData(object):
         # if these values are set and used with --diff we can show
         # changes made to particular files
         self.diff = diff
 
         if type(self.result) in [ str, unicode ]:
-            self.result = utils.parse_json(self.result)
-
+            self.result = utils.parse_json(self.result, from_remote=True)
 
         if self.host is None:
             raise Exception("host not set")
         if type(self.result) != dict:
             raise Exception("dictionary result expected")
diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py
index bf68eea4c..9d09254e3 100644
--- a/lib/ansible/utils/__init__.py
+++ b/lib/ansible/utils/__init__.py
@@ -311,30 +311,60 @@ def path_dwim_relative(original, dirname, source, playbook_base, check=True):
 def json_loads(data):
     ''' parse a JSON string and return a data structure '''
 
     return json.loads(data)
 
-def parse_json(raw_data):
+def _clean_data(orig_data):
+    ''' remove template tags from a string '''
+    data = orig_data
+    if isinstance(orig_data, basestring):
+        for pattern,replacement in (('{{','{#'), ('}}','#}'), ('{%','{#'), ('%}','#}')):
+            data = data.replace(pattern, replacement)
+    return data
+
+def _clean_data_struct(orig_data):
+    '''
+    walk a complex data structure, and use _clean_data() to
+    remove any template tags that may exist
+    '''
+    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])
+            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])
+    elif isinstance(orig_data, basestring):
+        data = _clean_data(orig_data)
+    else:
+        data = orig_data
+    return data
+
+def parse_json(raw_data, from_remote=False):
     ''' this version for module return data only '''
 
     orig_data = raw_data
 
     # ignore stuff like tcgetattr spewage or other warnings
     data = filter_leading_non_json_lines(raw_data)
 
     try:
-        return json.loads(data)
+        results = json.loads(data)
     except:
         # not JSON, but try "Baby JSON" which allows many of our modules to not
         # require JSON and makes writing modules in bash much simpler
         results = {}
         try:
             tokens = shlex.split(data)
         except:
             print "failed to parse json: "+ data
             raise
-
         for t in tokens:
             if "=" not in t:
                 raise errors.AnsibleError("failed to parse: %s" % orig_data)
             (key,value) = t.split("=", 1)
             if key == 'changed' or 'failed':
@@ -345,11 +375,15 @@ def parse_json(raw_data):
             if key == 'rc':
                 value = int(value)
             results[key] = value
         if len(results.keys()) == 0:
             return { "failed" : True, "parsed" : False, "msg" : orig_data }
-        return results
+
+    if from_remote:
+        results = _clean_data_struct(results)
+
+    return results
 
 def smush_braces(data):
     ''' smush Jinaj2 braces so unresolved templates like {{ foo }} don't get parsed weird by key=value code '''
     while '{{ ' in data:
         data = data.replace('{{ ', '{{')
diff --git a/lib/ansible/utils/template.py b/lib/ansible/utils/template.py
index 58fe2287c..9203470d3 100644
--- a/lib/ansible/utils/template.py
+++ b/lib/ansible/utils/template.py
@@ -78,11 +78,10 @@ class Flags:
 
 # TODO: refactor this file
 
 FILTER_PLUGINS = None
 _LISTRE = re.compile(r"(\w+)\[(\d+)\]")
-JINJA2_OVERRIDE='#jinja2:'
 
 def lookup(name, *args, **kwargs):
     from ansible import utils
     instance = utils.plugins.lookup_loader.get(name.lower(), basedir=kwargs.get('basedir',None))
     vars = kwargs.get('vars', None)
@@ -229,20 +228,10 @@ def template_from_file(basedir, path, vars, vault_password=None):
     except UnicodeDecodeError:
         raise errors.AnsibleError("unable to process as utf-8: %s" % realpath)
     except:
         raise errors.AnsibleError("unable to read %s" % realpath)
 
-
-    # Get jinja env overrides from template
-    if data.startswith(JINJA2_OVERRIDE):
-        eol = data.find('\n')
-        line = data[len(JINJA2_OVERRIDE):eol]
-        data = data[eol+1:]
-        for pair in line.split(','):
-            (key,val) = pair.split(':')
-            setattr(environment,key.strip(),ast.literal_eval(val.strip()))
-
     environment.template_class = J2Template
     try:
         t = environment.from_string(data)
     except TemplateSyntaxError, e:
         # Throw an exception which includes a more user friendly error message

Vulnerable code:         self.raw  = utils.parse_json(self.data)
        data = utils.parse_json(res['stdout'])
            self.result = utils.parse_json(self.result)

def parse_json(raw_data):
        return json.loads(data)

        return results
JINJA2_OVERRIDE='#jinja2:'

    # Get jinja env overrides from template
    if data.startswith(JINJA2_OVERRIDE):
        eol = data.find('\n')
        line = data[len(JINJA2_OVERRIDE):eol]
        data = data[eol+1:]
        for pair in line.split(','):
            (key,val) = pair.split(':')
            setattr(environment,key.strip(),ast.literal_eval(val.strip()))

Fixed code:         self.raw  = utils.parse_json(self.data, from_remote=True)
        data = utils.parse_json(res['stdout'], from_remote=True)
            self.result = utils.parse_json(self.result, from_remote=True)
def _clean_data(orig_data):
    ''' remove template tags from a string '''
    data = orig_data
    if isinstance(orig_data, basestring):
        for pattern,replacement in (('{{','{#'), ('}}','#}'), ('{%','{#'), ('%}','#}')):
            data = data.replace(pattern, replacement)
    return data

def _clean_data_struct(orig_data):
    '''
    walk a complex data structure, and use _clean_data() to
    remove any template tags that may exist
    '''
    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])
            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])
    elif isinstance(orig_data, basestring):
        data = _clean_data(orig_data)
    else:
        data = orig_data
    return data

def parse_json(raw_data, from_remote=False):
        results = json.loads(data)

    if from_remote:
        results = _clean_data_struct(results)

    return results