]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
SECURITY: support sandboxing in format expressions
authorArmin Ronacher <armin.ronacher@active-4.com>
Thu, 29 Dec 2016 13:13:38 +0000 (14:13 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Thu, 29 Dec 2016 13:13:38 +0000 (14:13 +0100)
jinja2/nodes.py
jinja2/sandbox.py
tests/test_security.py

index d32046ce5c3c5c068a2b25d5e8ad14423c7cf389..6d4593bb7aef798950c551b92ae53551e696349f 100644 (file)
@@ -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)
 
index 7e40ab30850707104a139d1fa95c5c177a482aa5..c035ddea00722810823ccb68c5aba1d175f54e2c 100644 (file)
 """
 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)
index e5b463fc989263e372823458f8c605e39d975a53..b033864f0f9f6c50a8db6c0a8ea442de2cd7d8b4 100644 (file)
@@ -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;'