From a91953807aa75e573ad2cee12e1de3ccfc541af1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 29 Nov 2010 13:21:57 +0100 Subject: [PATCH] Implemented operator intercepting --- CHANGES | 4 ++ jinja2/compiler.py | 34 +++++++++----- jinja2/nodes.py | 8 ++++ jinja2/sandbox.py | 91 +++++++++++++++++++++++++++++++++++- jinja2/testsuite/security.py | 37 ++++++++++++++- 5 files changed, 161 insertions(+), 13 deletions(-) diff --git a/CHANGES b/CHANGES index 590dd18..b171ac2 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,10 @@ Version 2.6 of returning an undefined. This fixes problems when passing undefined objects to Python semantics expecting APIs. - traceback support now works properly for PyPy. (Tested with 1.4) +- implemented operator intercepting for sandboxed environments. This + allows application developers to disable builtin operators for better + security. (For instance limit the mathematical operators to actual + integers instead of longs) Version 2.5.5 ------------- diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 50618a8..657a8bc 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -1421,19 +1421,31 @@ class CodeGenerator(NodeVisitor): self.visit(item.value, frame) self.write('}') - def binop(operator): + def binop(operator, interceptable=True): def visitor(self, node, frame): - self.write('(') - self.visit(node.left, frame) - self.write(' %s ' % operator) - self.visit(node.right, frame) + if self.environment.sandboxed and \ + operator in self.environment.intercepted_binops: + self.write('environment.call_binop(context, %r, ' % operator) + self.visit(node.left, frame) + self.write(', ') + self.visit(node.right, frame) + else: + self.write('(') + self.visit(node.left, frame) + self.write(' %s ' % operator) + self.visit(node.right, frame) self.write(')') return visitor - def uaop(operator): + def uaop(operator, interceptable=True): def visitor(self, node, frame): - self.write('(' + operator) - self.visit(node.node, frame) + if self.environment.sandboxed and \ + operator in self.environment.intercepted_unops: + self.write('environment.call_unop(context, %r, ' % operator) + self.visit(node.node, frame) + else: + self.write('(' + operator) + self.visit(node.node, frame) self.write(')') return visitor @@ -1444,11 +1456,11 @@ class CodeGenerator(NodeVisitor): visit_FloorDiv = binop('//') visit_Pow = binop('**') visit_Mod = binop('%') - visit_And = binop('and') - visit_Or = binop('or') + visit_And = binop('and', interceptable=False) + visit_Or = binop('or', interceptable=False) visit_Pos = uaop('+') visit_Neg = uaop('-') - visit_Not = uaop('not ') + visit_Not = uaop('not ', interceptable=False) del binop, uaop def visit_Concat(self, node, frame): diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 7c6b358..c18d299 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -372,6 +372,10 @@ class BinExpr(Expr): def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) + # intercepted operators cannot be folded at compile time + if self.environment.sandboxed and \ + self.operator in self.environment.intercepted_binops: + raise Impossible() f = _binop_to_func[self.operator] try: return f(self.left.as_const(eval_ctx), self.right.as_const(eval_ctx)) @@ -387,6 +391,10 @@ class UnaryExpr(Expr): def as_const(self, eval_ctx=None): eval_ctx = get_eval_context(self, eval_ctx) + # intercepted operators cannot be folded at compile time + if self.environment.sandboxed and \ + self.operator in self.environment.intercepted_unops: + raise Impossible() f = _uaop_to_func[self.operator] try: return f(self.node.as_const(eval_ctx)) diff --git a/jinja2/sandbox.py b/jinja2/sandbox.py index 0c9f573..3136f64 100644 --- a/jinja2/sandbox.py +++ b/jinja2/sandbox.py @@ -13,7 +13,6 @@ :license: BSD. """ import operator -from jinja2.runtime import Undefined from jinja2.environment import Environment from jinja2.exceptions import SecurityError from jinja2.utils import FunctionType, MethodType, TracebackType, CodeType, \ @@ -182,9 +181,81 @@ class SandboxedEnvironment(Environment): """ sandboxed = True + #: default callback table for the binary operators. A copy of this is + #: available on each instance of a sandboxed environment as + #: :attr:`binop_table` + default_binop_table = { + '+': operator.add, + '-': operator.sub, + '*': operator.mul, + '/': operator.truediv, + '//': operator.floordiv, + '**': operator.pow, + '%': operator.mod + } + + #: default callback table for the unary operators. A copy of this is + #: available on each instance of a sandboxed environment as + #: :attr:`unop_table` + default_unop_table = { + '+': operator.pos, + '-': operator.neg + } + + #: a set of binary operators that should be intercepted. Each operator + #: that is added to this set (empty by default) is delegated to the + #: :meth:`call_binop` method that will perform the operator. The default + #: operator callback is specified by :attr:`binop_table`. + #: + #: The following binary operators are interceptable: + #: ``//``, ``%``, ``+``, ``*``, ``-``, ``/``, and ``**`` + #: + #: The default operation form the operator table corresponds to the + #: builtin function. Intercepted calls are always slower than the native + #: operator call, so make sure only to intercept the ones you are + #: interested in. + #: + #: .. versionadded:: 2.6 + intercepted_binops = frozenset() + + #: a set of unary operators that should be intercepted. Each operator + #: that is added to this set (empty by default) is delegated to the + #: :meth:`call_unop` method that will perform the operator. The default + #: operator callback is specified by :attr:`unop_table`. + #: + #: The following unary operators are interceptable: ``+``, ``-`` + #: + #: The default operation form the operator table corresponds to the + #: builtin function. Intercepted calls are always slower than the native + #: operator call, so make sure only to intercept the ones you are + #: interested in. + #: + #: .. versionadded:: 2.6 + intercepted_unops = frozenset() + + def intercept_unop(self, operator): + """Called during template compilation with the name of a unary + operator to check if it should be intercepted at runtime. If this + method returns `True`, :meth:`call_unop` is excuted for this unary + operator. The default implementation of :meth:`call_unop` will use + the :attr:`unop_table` dictionary to perform the operator with the + same logic as the builtin one. + + The following unary operators are interceptable: ``+`` and ``-`` + + Intercepted calls are always slower than the native operator call, + so make sure only to intercept the ones you are interested in. + + .. versionadded:: 2.6 + """ + return False + + def __init__(self, *args, **kwargs): Environment.__init__(self, *args, **kwargs) self.globals['range'] = safe_range + self.binop_table = self.default_binop_table.copy() + self.unop_table = self.default_unop_table.copy() def is_safe_attribute(self, obj, attr, value): """The sandboxed environment will call this method to check if the @@ -204,6 +275,24 @@ class SandboxedEnvironment(Environment): return not (getattr(obj, 'unsafe_callable', False) or getattr(obj, 'alters_data', False)) + def call_binop(self, context, operator, left, right): + """For intercepted binary operator calls (:meth:`intercepted_binops`) + this function is executed instead of the builtin operator. This can + be used to fine tune the behavior of certain operators. + + .. versionadded:: 2.6 + """ + return self.binop_table[operator](left, right) + + def call_unop(self, context, operator, arg): + """For intercepted unary operator calls (:meth:`intercepted_unops`) + this function is executed instead of the builtin operator. This can + be used to fine tune the behavior of certain operators. + + .. versionadded:: 2.6 + """ + return self.unop_table[operator](arg) + def getitem(self, obj, argument): """Subscribe an object from sandboxed code.""" try: diff --git a/jinja2/testsuite/security.py b/jinja2/testsuite/security.py index bc10095..4518eac 100644 --- a/jinja2/testsuite/security.py +++ b/jinja2/testsuite/security.py @@ -16,7 +16,8 @@ from jinja2 import Environment from jinja2.sandbox import SandboxedEnvironment, \ ImmutableSandboxedEnvironment, unsafe from jinja2 import Markup, escape -from jinja2.exceptions import SecurityError, TemplateSyntaxError +from jinja2.exceptions import SecurityError, TemplateSyntaxError, \ + TemplateRuntimeError class PrivateStuff(object): @@ -123,6 +124,40 @@ class SandboxTestCase(JinjaTestCase): tmpl = env.from_string('{{ cls|attr("__subclasses__")() }}') self.assert_raises(SecurityError, tmpl.render, cls=int) + def test_binary_operator_intercepting(self): + 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('{{ %s }}' % expr) + assert t.render(ctx) == rv + env.intercepted_binops = frozenset(['+']) + t = env.from_string('{{ %s }}' % expr) + try: + t.render(ctx) + except TemplateRuntimeError, e: + pass + else: + self.fail('expected runtime error') + + def test_unary_operator_intercepting(self): + 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('{{ %s }}' % expr) + assert t.render(ctx) == rv + env.intercepted_unops = frozenset(['-']) + t = env.from_string('{{ %s }}' % expr) + try: + t.render(ctx) + except TemplateRuntimeError, e: + pass + else: + self.fail('expected runtime error') + def suite(): suite = unittest.TestSuite() -- 2.26.2