Skip to content

Commit 9b53045

Browse files
committed
SECURITY: support sandboxing in format expressions
1 parent 8189d21 commit 9b53045

File tree

3 files changed

+143
-5
lines changed

3 files changed

+143
-5
lines changed

Diff for: jinja2/nodes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ class Call(Expr):
604604

605605
def as_const(self, eval_ctx=None):
606606
eval_ctx = get_eval_context(self, eval_ctx)
607-
if eval_ctx.volatile:
607+
if eval_ctx.volatile or eval_ctx.environment.sandboxed:
608608
raise Impossible()
609609
obj = self.node.as_const(eval_ctx)
610610

Diff for: jinja2/sandbox.py

+116-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,17 @@
1414
"""
1515
import types
1616
import operator
17+
from collections import Mapping
1718
from jinja2.environment import Environment
1819
from jinja2.exceptions import SecurityError
19-
from jinja2._compat import string_types, PY2
20+
from jinja2._compat import string_types, text_type, PY2
21+
from jinja2.utils import Markup
22+
23+
has_format = False
24+
if hasattr(text_type, 'format'):
25+
from markupsafe import EscapeFormatter
26+
from string import Formatter
27+
has_format = True
2028

2129

2230
#: maximum number of items a range may produce
@@ -38,6 +46,12 @@
3846
#: unsafe generator attirbutes.
3947
UNSAFE_GENERATOR_ATTRIBUTES = set(['gi_frame', 'gi_code'])
4048

49+
#: unsafe attributes on coroutines
50+
UNSAFE_COROUTINE_ATTRIBUTES = set(['cr_frame', 'cr_code'])
51+
52+
#: unsafe attributes on async generators
53+
UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = set(['ag_code', 'ag_frame'])
54+
4155
import warnings
4256

4357
# make sure we don't warn in python 2.6 about stuff we don't care about
@@ -94,6 +108,49 @@
94108
)
95109

96110

111+
class _MagicFormatMapping(Mapping):
112+
"""This class implements a dummy wrapper to fix a bug in the Python
113+
standard library for string formatting.
114+
115+
See http://bugs.python.org/issue13598 for information about why
116+
this is necessary.
117+
"""
118+
119+
def __init__(self, args, kwargs):
120+
self._args = args
121+
self._kwargs = kwargs
122+
self._last_index = 0
123+
124+
def __getitem__(self, key):
125+
if key == '':
126+
idx = self._last_index
127+
self._last_index += 1
128+
try:
129+
return self._args[idx]
130+
except LookupError:
131+
pass
132+
key = str(idx)
133+
return self._kwargs[key]
134+
135+
def __iter__(self):
136+
return iter(self._kwargs)
137+
138+
def __len__(self):
139+
return len(self._kwargs)
140+
141+
142+
def inspect_format_method(callable):
143+
if not has_format:
144+
return None
145+
if not isinstance(callable, (types.MethodType,
146+
types.BuiltinMethodType)) or \
147+
callable.__name__ != 'format':
148+
return None
149+
obj = callable.__self__
150+
if isinstance(obj, string_types):
151+
return obj
152+
153+
97154
def safe_range(*args):
98155
"""A range that can't generate ranges with a length of more than
99156
MAX_RANGE items.
@@ -145,6 +202,12 @@ def is_internal_attribute(obj, attr):
145202
elif isinstance(obj, types.GeneratorType):
146203
if attr in UNSAFE_GENERATOR_ATTRIBUTES:
147204
return True
205+
elif hasattr(types, 'CoroutineType') and isinstance(obj, types.CoroutineType):
206+
if attr in UNSAFE_COROUTINE_ATTRIBUTES:
207+
return True
208+
elif hasattr(types, 'AsyncGeneratorType') and isinstance(obj, types.AsyncGeneratorType):
209+
if attri in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES:
210+
return True
148211
return attr.startswith('__')
149212

150213

@@ -183,8 +246,8 @@ class SandboxedEnvironment(Environment):
183246
attributes or functions are safe to access.
184247
185248
If the template tries to access insecure code a :exc:`SecurityError` is
186-
raised. However also other exceptions may occour during the rendering so
187-
the caller has to ensure that all exceptions are catched.
249+
raised. However also other exceptions may occur during the rendering so
250+
the caller has to ensure that all exceptions are caught.
188251
"""
189252
sandboxed = True
190253

@@ -346,8 +409,24 @@ def unsafe_undefined(self, obj, attribute):
346409
obj.__class__.__name__
347410
), name=attribute, obj=obj, exc=SecurityError)
348411

412+
def format_string(self, s, args, kwargs):
413+
"""If a format call is detected, then this is routed through this
414+
method so that our safety sandbox can be used for it.
415+
"""
416+
if isinstance(s, Markup):
417+
formatter = SandboxedEscapeFormatter(self, s.escape)
418+
else:
419+
formatter = SandboxedFormatter(self)
420+
kwargs = _MagicFormatMapping(args, kwargs)
421+
rv = formatter.vformat(s, args, kwargs)
422+
return type(s)(rv)
423+
349424
def call(__self, __context, __obj, *args, **kwargs):
350425
"""Call an object from sandboxed code."""
426+
fmt = inspect_format_method(__obj)
427+
if fmt is not None:
428+
return __self.format_string(fmt, args, kwargs)
429+
351430
# the double prefixes are to avoid double keyword argument
352431
# errors when proxying the call.
353432
if not __self.is_safe_callable(__obj):
@@ -365,3 +444,37 @@ def is_safe_attribute(self, obj, attr, value):
365444
if not SandboxedEnvironment.is_safe_attribute(self, obj, attr, value):
366445
return False
367446
return not modifies_known_mutable(obj, attr)
447+
448+
449+
if has_format:
450+
# This really is not a public API apparenlty.
451+
try:
452+
from _string import formatter_field_name_split
453+
except ImportError:
454+
def formatter_field_name_split(field_name):
455+
return field_name._formatter_field_name_split()
456+
457+
class SandboxedFormatterMixin(object):
458+
459+
def __init__(self, env):
460+
self._env = env
461+
462+
def get_field(self, field_name, args, kwargs):
463+
first, rest = formatter_field_name_split(field_name)
464+
obj = self.get_value(first, args, kwargs)
465+
for is_attr, i in rest:
466+
if is_attr:
467+
obj = self._env.getattr(obj, i)
468+
else:
469+
obj = self._env.getitem(obj, i)
470+
return obj, first
471+
472+
class SandboxedFormatter(SandboxedFormatterMixin, Formatter):
473+
def __init__(self, env):
474+
SandboxedFormatterMixin.__init__(self, env)
475+
Formatter.__init__(self)
476+
477+
class SandboxedEscapeFormatter(SandboxedFormatterMixin, EscapeFormatter):
478+
def __init__(self, env, escape):
479+
SandboxedFormatterMixin.__init__(self, env)
480+
EscapeFormatter.__init__(self, escape)

Diff for: tests/test_security.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from jinja2 import Environment
1414
from jinja2.sandbox import SandboxedEnvironment, \
15-
ImmutableSandboxedEnvironment, unsafe
15+
ImmutableSandboxedEnvironment, unsafe, has_format
1616
from jinja2 import Markup, escape
1717
from jinja2.exceptions import SecurityError, TemplateSyntaxError, \
1818
TemplateRuntimeError
@@ -159,3 +159,28 @@ def disable_op(arg):
159159
pass
160160
else:
161161
assert False, 'expected runtime error'
162+
163+
164+
@pytest.mark.sandbox
165+
@pytest.mark.skipif(not has_format, reason='No format support')
166+
class TestStringFormat(object):
167+
168+
def test_basic_format_safety(self):
169+
env = SandboxedEnvironment()
170+
t = env.from_string('{{ "a{0.__class__}b".format(42) }}')
171+
assert t.render() == 'ab'
172+
173+
def test_basic_format_all_okay(self):
174+
env = SandboxedEnvironment()
175+
t = env.from_string('{{ "a{0.foo}b".format({"foo": 42}) }}')
176+
assert t.render() == 'a42b'
177+
178+
def test_basic_format_safety(self):
179+
env = SandboxedEnvironment()
180+
t = env.from_string('{{ ("a{0.__class__}b{1}"|safe).format(42, "<foo>") }}')
181+
assert t.render() == 'ab&lt;foo&gt;'
182+
183+
def test_basic_format_all_okay(self):
184+
env = SandboxedEnvironment()
185+
t = env.from_string('{{ ("a{0.foo}b{1}"|safe).format({"foo": 42}, "<foo>") }}')
186+
assert t.render() == 'a42b&lt;foo&gt;'

0 commit comments

Comments
 (0)