CVE-2016-10745 (Python, published 2019-04-08T13:29Z)
Description: value: In Pallets Jinja before 2.8.1, str.format allows a sandbox escape.
Severity: MEDIUM (CVSS v3 base score: 8.6)
Weakness: CWE-134 \u2014 Use of Externally-Controlled Format String
The product uses a function that accepts a format string as an argument, but the format string originates from an external source.
Repository: https://github.com/pallets/jinja
Files changed:
jinja2/nodes.py
jinja2/sandbox.py
tests/test_security.py
Commit message: SECURITY: support sandboxing in format expressions


Vulnerable code:
        if eval_ctx.volatile:
from jinja2._compat import string_types, PY2
    raised.  However also other exceptions may occour during the rendering so
    the caller has to ensure that all exceptions are catched.
     ImmutableSandboxedEnvironment, unsafe

Fixed code:
        if eval_ctx.volatile or eval_ctx.environment.sandboxed:
from collections import Mapping
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
#: 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'])

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


    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
    raised.  However also other exceptions may occur during the rendering so
    the caller has to ensure that all exceptions are caught.
    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)

        fmt = inspect_format_method(__obj)
        if fmt is not None:
            return __self.format_string(fmt, args, kwargs)



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)
     ImmutableSandboxedEnvironment, unsafe, has_format


@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;'