CVE-2016-10745 (published 2019-04-08T13:29Z)

In Pallets Jinja before 2.8.1, str.format allows a sandbox escape.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 8.6, HIGH (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-134
cwe_name: Use of Externally-Controlled Format String
description: The product uses a function that accepts a format string as an argument, but the format string originates from an external source.
extended_description: 
            When an attacker can modify an externally-controlled format string, this can lead to buffer overflows, denial of service, or data representation problems.
            It should be noted that in some circumstances, such as internationalization, the set of format strings is externally controlled by design. If the source of these format strings is trusted (e.g. only contained in library files that are only modifiable by the system administrator), then the external control might not itself pose a vulnerability.
         
         

Fix information:
repo_url: https://github.com/pallets/jinja
commit_details: repo_url: https://github.com/pallets/jinja
author: Armin Ronacher
author_date: 2016-12-29 14:13:38+01:00
msg: SECURITY: support sandboxing in format expressions
repository: repo_url: https://github.com/pallets/jinja
repo_name: pallets/jinja
description: A very fast and expressive template engine.
repo_language: Python
owner: pallets
file_changes: filename: nodes.py
change_type: ModificationType.MODIFY
diff: @@ -604,7 +604,7 @@ class Call(Expr):
 
     def as_const(self, eval_ctx=None):
         eval_ctx = get_eval_context(self, eval_ctx)
-        if eval_ctx.volatile:
+        if eval_ctx.volatile or eval_ctx.environment.sandboxed:
             raise Impossible()
         obj = self.node.as_const(eval_ctx)
 

programming_language: Python
filename: sandbox.py
change_type: ModificationType.MODIFY
diff: @@ -14,9 +14,17 @@
 """
 import types
 import operator
+from collections import Mapping
 from jinja2.environment import Environment
 from jinja2.exceptions import SecurityError
-from jinja2._compat import string_types, PY2
+from jinja2._compat import string_types, text_type, PY2
+from jinja2.utils import Markup
+
+has_format = False
+if hasattr(text_type, 'format'):
+    from markupsafe import EscapeFormatter
+    from string import Formatter
+    has_format = True
 
 
 #: maximum number of items a range may produce
@@ -38,6 +46,12 @@ UNSAFE_METHOD_ATTRIBUTES = set(['im_class', 'im_func', 'im_self'])
 #: unsafe generator attirbutes.
 UNSAFE_GENERATOR_ATTRIBUTES = set(['gi_frame', 'gi_code'])
 
+#: unsafe attributes on coroutines
+UNSAFE_COROUTINE_ATTRIBUTES = set(['cr_frame', 'cr_code'])
+
+#: unsafe attributes on async generators
+UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = set(['ag_code', 'ag_frame'])
+
 import warnings
 
 # make sure we don't warn in python 2.6 about stuff we don't care about
@@ -94,6 +108,49 @@ _mutable_spec = (
 )
 
 
+class _MagicFormatMapping(Mapping):
+    """This class implements a dummy wrapper to fix a bug in the Python
+    standard library for string formatting.
+
+    See http://bugs.python.org/issue13598 for information about why
+    this is necessary.
+    """
+
+    def __init__(self, args, kwargs):
+        self._args = args
+        self._kwargs = kwargs
+        self._last_index = 0
+
+    def __getitem__(self, key):
+        if key == '':
+            idx = self._last_index
+            self._last_index += 1
+            try:
+                return self._args[idx]
+            except LookupError:
+                pass
+            key = str(idx)
+        return self._kwargs[key]
+
+    def __iter__(self):
+        return iter(self._kwargs)
+
+    def __len__(self):
+        return len(self._kwargs)
+
+
+def inspect_format_method(callable):
+    if not has_format:
+        return None
+    if not isinstance(callable, (types.MethodType,
+                                 types.BuiltinMethodType)) or \
+       callable.__name__ != 'format':
+        return None
+    obj = callable.__self__
+    if isinstance(obj, string_types):
+        return obj
+
+
 def safe_range(*args):
     """A range that can't generate ranges with a length of more than
     MAX_RANGE items.
@@ -145,6 +202,12 @@ def is_internal_attribute(obj, attr):
     elif isinstance(obj, types.GeneratorType):
         if attr in UNSAFE_GENERATOR_ATTRIBUTES:
             return True
+    elif hasattr(types, 'CoroutineType') and isinstance(obj, types.CoroutineType):
+        if attr in UNSAFE_COROUTINE_ATTRIBUTES:
+            return True
+    elif hasattr(types, 'AsyncGeneratorType') and isinstance(obj, types.AsyncGeneratorType):
+        if attri in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES:
+            return True
     return attr.startswith('__')
 
 
@@ -183,8 +246,8 @@ class SandboxedEnvironment(Environment):
     attributes or functions are safe to access.
 
     If the template tries to access insecure code a :exc:`SecurityError` is
-    raised.  However also other exceptions may occour during the rendering so
-    the caller has to ensure that all exceptions are catched.
+    raised.  However also other exceptions may occur during the rendering so
+    the caller has to ensure that all exceptions are caught.
     """
     sandboxed = True
 
@@ -346,8 +409,24 @@ class SandboxedEnvironment(Environment):
             obj.__class__.__name__
         ), name=attribute, obj=obj, exc=SecurityError)
 
+    def format_string(self, s, args, kwargs):
+        """If a format call is detected, then this is routed through this
+        method so that our safety sandbox can be used for it.
+        """
+        if isinstance(s, Markup):
+            formatter = SandboxedEscapeFormatter(self, s.escape)
+        else:
+            formatter = SandboxedFormatter(self)
+        kwargs = _MagicFormatMapping(args, kwargs)
+        rv = formatter.vformat(s, args, kwargs)
+        return type(s)(rv)
+
     def call(__self, __context, __obj, *args, **kwargs):
         """Call an object from sandboxed code."""
+        fmt = inspect_format_method(__obj)
+        if fmt is not None:
+            return __self.format_string(fmt, args, kwargs)
+
         # the double prefixes are to avoid double keyword argument
         # errors when proxying the call.
         if not __self.is_safe_callable(__obj):
@@ -365,3 +444,37 @@ class ImmutableSandboxedEnvironment(SandboxedEnvironment):
         if not SandboxedEnvironment.is_safe_attribute(self, obj, attr, value):
             return False
         return not modifies_known_mutable(obj, attr)
+
+
+if has_format:
+    # This really is not a public API apparenlty.
+    try:
+        from _string import formatter_field_name_split
+    except ImportError:
+        def formatter_field_name_split(field_name):
+            return field_name._formatter_field_name_split()
+
+    class SandboxedFormatterMixin(object):
+
+        def __init__(self, env):
+            self._env = env
+
+        def get_field(self, field_name, args, kwargs):
+            first, rest = formatter_field_name_split(field_name)
+            obj = self.get_value(first, args, kwargs)
+            for is_attr, i in rest:
+                if is_attr:
+                    obj = self._env.getattr(obj, i)
+                else:
+                    obj = self._env.getitem(obj, i)
+            return obj, first
+
+    class SandboxedFormatter(SandboxedFormatterMixin, Formatter):
+        def __init__(self, env):
+            SandboxedFormatterMixin.__init__(self, env)
+            Formatter.__init__(self)
+
+    class SandboxedEscapeFormatter(SandboxedFormatterMixin, EscapeFormatter):
+        def __init__(self, env, escape):
+            SandboxedFormatterMixin.__init__(self, env)
+            EscapeFormatter.__init__(self, escape)

programming_language: Python
filename: test_security.py
change_type: ModificationType.MODIFY
diff: @@ -12,7 +12,7 @@ import pytest
 
 from jinja2 import Environment
 from jinja2.sandbox import SandboxedEnvironment, \
-     ImmutableSandboxedEnvironment, unsafe
+     ImmutableSandboxedEnvironment, unsafe, has_format
 from jinja2 import Markup, escape
 from jinja2.exceptions import SecurityError, TemplateSyntaxError, \
      TemplateRuntimeError
@@ -159,3 +159,28 @@ class TestSandbox():
                 pass
             else:
                 assert False, 'expected runtime error'
+
+
+@pytest.mark.sandbox
+@pytest.mark.skipif(not has_format, reason='No format support')
+class TestStringFormat(object):
+
+    def test_basic_format_safety(self):
+        env = SandboxedEnvironment()
+        t = env.from_string('{{ "a{0.__class__}b".format(42) }}')
+        assert t.render() == 'ab'
+
+    def test_basic_format_all_okay(self):
+        env = SandboxedEnvironment()
+        t = env.from_string('{{ "a{0.foo}b".format({"foo": 42}) }}')
+        assert t.render() == 'a42b'
+
+    def test_basic_format_safety(self):
+        env = SandboxedEnvironment()
+        t = env.from_string('{{ ("a{0.__class__}b{1}"|safe).format(42, "<foo>") }}')
+        assert t.render() == 'ab&lt;foo&gt;'
+
+    def test_basic_format_all_okay(self):
+        env = SandboxedEnvironment()
+        t = env.from_string('{{ ("a{0.foo}b{1}"|safe).format({"foo": 42}, "<foo>") }}')
+        assert t.render() == 'a42b&lt;foo&gt;'

programming_language: Python