Skip to content

Commit

Permalink
SECURITY: support sandboxing in format expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Dec 29, 2016
1 parent 8189d21 commit 9b53045
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 5 deletions.
2 changes: 1 addition & 1 deletion jinja2/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
119 changes: 116 additions & 3 deletions jinja2/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,6 +46,12 @@
#: 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
Expand Down Expand Up @@ -94,6 +108,49 @@
)


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.
Expand Down Expand Up @@ -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('__')


Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -346,8 +409,24 @@ def unsafe_undefined(self, obj, attribute):
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):
Expand All @@ -365,3 +444,37 @@ def is_safe_attribute(self, obj, attr, value):
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)
27 changes: 26 additions & 1 deletion tests/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

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
Expand Down Expand Up @@ -159,3 +159,28 @@ def disable_op(arg):
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;'

0 comments on commit 9b53045

Please sign in to comment.