summaryrefslogtreecommitdiffstats
path: root/tests/test_security.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_security.py')
-rw-r--r--tests/test_security.py173
1 files changed, 173 insertions, 0 deletions
diff --git a/tests/test_security.py b/tests/test_security.py
new file mode 100644
index 0000000..0e8dc5c
--- /dev/null
+++ b/tests/test_security.py
@@ -0,0 +1,173 @@
+import pytest
+from markupsafe import escape
+
+from jinja2 import Environment
+from jinja2.exceptions import SecurityError
+from jinja2.exceptions import TemplateRuntimeError
+from jinja2.exceptions import TemplateSyntaxError
+from jinja2.nodes import EvalContext
+from jinja2.sandbox import ImmutableSandboxedEnvironment
+from jinja2.sandbox import SandboxedEnvironment
+from jinja2.sandbox import unsafe
+
+
+class PrivateStuff:
+ def bar(self):
+ return 23
+
+ @unsafe
+ def foo(self):
+ return 42
+
+ def __repr__(self):
+ return "PrivateStuff"
+
+
+class PublicStuff:
+ def bar(self):
+ return 23
+
+ def _foo(self):
+ return 42
+
+ def __repr__(self):
+ return "PublicStuff"
+
+
+class TestSandbox:
+ def test_unsafe(self, env):
+ env = SandboxedEnvironment()
+ pytest.raises(
+ SecurityError, env.from_string("{{ foo.foo() }}").render, foo=PrivateStuff()
+ )
+ assert env.from_string("{{ foo.bar() }}").render(foo=PrivateStuff()) == "23"
+
+ pytest.raises(
+ SecurityError, env.from_string("{{ foo._foo() }}").render, foo=PublicStuff()
+ )
+ assert env.from_string("{{ foo.bar() }}").render(foo=PublicStuff()) == "23"
+ assert env.from_string("{{ foo.__class__ }}").render(foo=42) == ""
+ assert env.from_string("{{ foo.func_code }}").render(foo=lambda: None) == ""
+ # security error comes from __class__ already.
+ pytest.raises(
+ SecurityError,
+ env.from_string("{{ foo.__class__.__subclasses__() }}").render,
+ foo=42,
+ )
+
+ def test_immutable_environment(self, env):
+ env = ImmutableSandboxedEnvironment()
+ pytest.raises(SecurityError, env.from_string("{{ [].append(23) }}").render)
+ pytest.raises(SecurityError, env.from_string("{{ {1:2}.clear() }}").render)
+
+ def test_restricted(self, env):
+ env = SandboxedEnvironment()
+ pytest.raises(
+ TemplateSyntaxError,
+ env.from_string,
+ "{% for item.attribute in seq %}...{% endfor %}",
+ )
+ pytest.raises(
+ TemplateSyntaxError,
+ env.from_string,
+ "{% for foo, bar.baz in seq %}...{% endfor %}",
+ )
+
+ def test_template_data(self, env):
+ env = Environment(autoescape=True)
+ t = env.from_string(
+ "{% macro say_hello(name) %}"
+ "<p>Hello {{ name }}!</p>{% endmacro %}"
+ '{{ say_hello("<blink>foo</blink>") }}'
+ )
+ escaped_out = "<p>Hello &lt;blink&gt;foo&lt;/blink&gt;!</p>"
+ assert t.render() == escaped_out
+ assert str(t.module) == escaped_out
+ assert escape(t.module) == escaped_out
+ assert t.module.say_hello("<blink>foo</blink>") == escaped_out
+ assert (
+ escape(t.module.say_hello(EvalContext(env), "<blink>foo</blink>"))
+ == escaped_out
+ )
+ assert escape(t.module.say_hello("<blink>foo</blink>")) == escaped_out
+
+ def test_attr_filter(self, env):
+ env = SandboxedEnvironment()
+ tmpl = env.from_string('{{ cls|attr("__subclasses__")() }}')
+ pytest.raises(SecurityError, tmpl.render, cls=int)
+
+ def test_binary_operator_intercepting(self, env):
+ def disable_op(left, right):
+ raise TemplateRuntimeError("that operator so does not work")
+
+ for expr, ctx, rv in ("1 + 2", {}, "3"), ("a + 2", {"a": 2}, "4"):
+ env = SandboxedEnvironment()
+ env.binop_table["+"] = disable_op
+ t = env.from_string(f"{{{{ {expr} }}}}")
+ assert t.render(ctx) == rv
+ env.intercepted_binops = frozenset(["+"])
+ t = env.from_string(f"{{{{ {expr} }}}}")
+ with pytest.raises(TemplateRuntimeError):
+ t.render(ctx)
+
+ def test_unary_operator_intercepting(self, env):
+ def disable_op(arg):
+ raise TemplateRuntimeError("that operator so does not work")
+
+ for expr, ctx, rv in ("-1", {}, "-1"), ("-a", {"a": 2}, "-2"):
+ env = SandboxedEnvironment()
+ env.unop_table["-"] = disable_op
+ t = env.from_string(f"{{{{ {expr} }}}}")
+ assert t.render(ctx) == rv
+ env.intercepted_unops = frozenset(["-"])
+ t = env.from_string(f"{{{{ {expr} }}}}")
+ with pytest.raises(TemplateRuntimeError):
+ t.render(ctx)
+
+
+class TestStringFormat:
+ 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_safe_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_safe_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;"
+
+ def test_empty_braces_format(self):
+ env = SandboxedEnvironment()
+ t1 = env.from_string('{{ ("a{}b{}").format("foo", "42")}}')
+ t2 = env.from_string('{{ ("a{}b{}"|safe).format(42, "<foo>") }}')
+ assert t1.render() == "afoob42"
+ assert t2.render() == "a42b&lt;foo&gt;"
+
+
+class TestStringFormatMap:
+ def test_basic_format_safety(self):
+ env = SandboxedEnvironment()
+ t = env.from_string('{{ "a{x.__class__}b".format_map({"x":42}) }}')
+ assert t.render() == "ab"
+
+ def test_basic_format_all_okay(self):
+ env = SandboxedEnvironment()
+ t = env.from_string('{{ "a{x.foo}b".format_map({"x":{"foo": 42}}) }}')
+ assert t.render() == "a42b"
+
+ def test_safe_format_all_okay(self):
+ env = SandboxedEnvironment()
+ t = env.from_string(
+ '{{ ("a{x.foo}b{y}"|safe).format_map({"x":{"foo": 42}, "y":"<foo>"}) }}'
+ )
+ assert t.render() == "a42b&lt;foo&gt;"