diff options
Diffstat (limited to 'tests/test_security.py')
-rw-r--r-- | tests/test_security.py | 173 |
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 <blink>foo</blink>!</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<foo>" + + 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<foo>" + + 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<foo>" + + +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<foo>" |