diff --git a/jinja2/_compat.py b/jinja2/_compat.py new file mode 100644 index 0000000..4dbf6ea --- /dev/null +++ b/jinja2/_compat.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +""" + jinja2._compat + ~~~~~~~~~~~~~~ + + Some py2/py3 compatibility support based on a stripped down + version of six so we don't have to depend on a specific version + of it. + + :copyright: Copyright 2013 by the Jinja team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +import sys + +PY2 = sys.version_info[0] == 2 +PYPY = hasattr(sys, 'pypy_translation_info') +_identity = lambda x: x + + +if not PY2: + unichr = chr + range_type = range + text_type = str + string_types = (str,) + integer_types = (int,) + + iterkeys = lambda d: iter(d.keys()) + itervalues = lambda d: iter(d.values()) + iteritems = lambda d: iter(d.items()) + + import pickle + from io import BytesIO, StringIO + NativeStringIO = StringIO + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + ifilter = filter + imap = map + izip = zip + intern = sys.intern + + implements_iterator = _identity + implements_to_string = _identity + encode_filename = _identity + +else: + unichr = unichr + text_type = unicode + range_type = xrange + string_types = (str, unicode) + integer_types = (int, long) + + iterkeys = lambda d: d.iterkeys() + itervalues = lambda d: d.itervalues() + iteritems = lambda d: d.iteritems() + + import cPickle as pickle + from cStringIO import StringIO as BytesIO, StringIO + NativeStringIO = BytesIO + + exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') + + from itertools import imap, izip, ifilter + intern = intern + + def implements_iterator(cls): + cls.next = cls.__next__ + del cls.__next__ + return cls + + def implements_to_string(cls): + cls.__unicode__ = cls.__str__ + cls.__str__ = lambda x: x.__unicode__().encode('utf-8') + return cls + + def encode_filename(filename): + if isinstance(filename, unicode): + return filename.encode('utf-8') + return filename + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a + # dummy metaclass for one level of class instantiation that replaces + # itself with the actual metaclass. + class metaclass(type): + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +try: + from urllib.parse import quote_from_bytes as url_quote +except ImportError: + from urllib import quote as url_quote + + +try: + from collections import abc +except ImportError: + import collections as abc diff --git a/jinja2/nodes.py b/jinja2/nodes.py index f9da1da..addee6a 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -595,7 +595,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) diff --git a/jinja2/sandbox.py b/jinja2/sandbox.py index a1cbb29..5f70ef8 100644 --- a/jinja2/sandbox.py +++ b/jinja2/sandbox.py @@ -12,12 +12,19 @@ :copyright: (c) 2010 by the Jinja Team. :license: BSD. """ +import types import operator +from collections import Mapping from jinja2.environment import Environment from jinja2.exceptions import SecurityError +from jinja2._compat import string_types, text_type, PY2 from jinja2.utils import FunctionType, MethodType, TracebackType, CodeType, \ - FrameType, GeneratorType + FrameType, GeneratorType, Markup +has_format = False +if hasattr(text_type, 'format'): + from string import Formatter + has_format = True #: maximum number of items a range may produce MAX_RANGE = 100000 @@ -29,6 +36,11 @@ UNSAFE_FUNCTION_ATTRIBUTES = set(['func_closure', 'func_code', 'func_dict', #: unsafe method attributes. function attributes are unsafe for methods too UNSAFE_METHOD_ATTRIBUTES = set(['im_class', 'im_func', 'im_self']) +#: 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 @@ -85,6 +97,79 @@ _mutable_spec = ( ])) ) +# Bundled EscapeFormatter class from markupsafe >= 0.21 which is used by +# jinja2 for fixing CVE-2016-10745 +# Copyright 2010 Pallets +# BSD 3-Clause License +# https://github.com/pallets/markupsafe/blob/79ee6ce0ed93c6da73512f069d7db866d955df04/LICENSE.rst +if hasattr(text_type, "format"): + + class EscapeFormatter(Formatter): + def __init__(self, escape): + self.escape = escape + + def format_field(self, value, format_spec): + if hasattr(value, "__html_format__"): + rv = value.__html_format__(format_spec) + elif hasattr(value, "__html__"): + if format_spec: + raise ValueError( + "Format specifier {0} given, but {1} does not" + " define __html_format__. A class that defines" + " __html__ must define __html_format__ to work" + " with format specifiers.".format(format_spec, type(value)) + ) + rv = value.__html__() + else: + # We need to make sure the format spec is unicode here as + # otherwise the wrong callback methods are invoked. For + # instance a byte string there would invoke __str__ and + # not __unicode__. + rv = Formatter.format_field(self, value, text_type(format_spec)) + return text_type(self.escape(rv)) + +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 @@ -139,6 +224,12 @@ def is_internal_attribute(obj, attr): elif isinstance(obj, GeneratorType): if attr == 'gi_frame': 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 attr in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES: + return True return attr.startswith('__') @@ -177,8 +268,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 @@ -340,8 +431,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): @@ -359,3 +466,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)