Mercurial > genshi > genshi-test
diff genshi/template/eval.py @ 642:1cf5fdfe7214 experimental-sandboxed
first implementaiton of a secure genshi
author | aronacher |
---|---|
date | Wed, 26 Sep 2007 14:07:10 +0000 |
parents | 9ada030ad986 |
children | e5363d3c22d3 |
line wrap: on
line diff
--- a/genshi/template/eval.py +++ b/genshi/template/eval.py @@ -24,6 +24,7 @@ from sets import Set as set import sys from textwrap import dedent +from types import FunctionType, MethodType from genshi.core import Markup from genshi.template.base import TemplateRuntimeError @@ -36,10 +37,10 @@ class Code(object): """Abstract base class for the `Expression` and `Suite` classes.""" - __slots__ = ['source', 'code', 'ast', '_globals'] + __slots__ = ['source', 'code', 'ast', 'secure', '_globals'] def __init__(self, source, filename=None, lineno=-1, lookup='strict', - xform=None): + xform=None, secure=False): """Create the code object, either from a string, or from an AST node. :param source: either a string containing the source code, or an AST @@ -53,6 +54,7 @@ :param xform: the AST transformer that should be applied to the code; if `None`, the appropriate transformation is chosen depending on the mode + :param secure: If security features should be enabled. """ if isinstance(source, basestring): self.source = source @@ -68,11 +70,18 @@ self.ast = node self.code = _compile(node, self.source, mode=self.mode, - filename=filename, lineno=lineno, xform=xform) + filename=filename, lineno=lineno, xform=xform, + secure=secure) if lookup is None: lookup = LenientLookup elif isinstance(lookup, basestring): - lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup] + lookup = { + 'lenient': LenientLookup, + 'strict': StrictLookup + }[lookup] + if secure: + lookup = SecurityLookupWrapper(lookup) + self.secure = secure self._globals = lookup.globals() def __eq__(self, other): @@ -365,6 +374,66 @@ undefined = classmethod(undefined) +class SecurityLookupWrapper(object): + """ + Special class that wraps a lookup so that insecure accesses result + in undefined. Additionally the globals are secured. + """ + + def __init__(self, lookup): + self._lookup = lookup + + def __getattr__(self, name): + return getattr(self._lookup, name) + + def globals(self): + namespace = self._lookup.globals() + namespace.update( + _lookup_name=self.lookup_name, + _lookup_attr=self.lookup_attr, + _lookup_item=self.lookup_item + ) + return namespace + + def lookup_name(self, data, name): + __traceback_hide__ = True + if name.startswith('_'): + val = self._lookup.undefined(name) + else: + val = data.get(name, UNDEFINED) + if val is UNDEFINED: + val = SECURE_BUILTINS.get(name, val) + if val is UNDEFINED: + val = self._lookup.undefined(name) + return val + + def lookup_attr(self, data, obj, key): + __traceback_hide__ = True + # XXX: if we weaken this, don't forget to create an + # _unsafe_test for sys._active_frames / sys._getframe + if key.startswith('_') or self._unsafe_test(obj, key): + return self._lookup.undefined(key) + return self._lookup.lookup_attr(data, obj, key) + + def lookup_item(cls, data, obj, key): + __traceback_hide__ = True + if key.startswith('_') or self._unsafe_test(obj, key): + return self._lookup.undefined(key) + return self._lookup.lookup_item(data, obj, key) + + def _unsafe_test(self, obj, key): + if isinstance(obj, MethodType): + if key in ('im_class', 'im_func', 'im_self'): + return True + obj = obj.im_func + if isinstance(obj, FunctionType): + return key in ('func_closure', 'func_code', 'func_defaults', + 'func_dict', 'func_doc', 'func_globals', + 'func_name') + known_unsafe = getattr(obj, '__genshi_unsafe__', ()) + return key in known_unsafe + + def _parse(source, mode='eval'): source = source.strip() if mode == 'exec': @@ -379,11 +448,11 @@ return parse(source, mode) def _compile(node, source=None, mode='eval', filename=None, lineno=-1, - xform=None): + xform=None, secure=False): if xform is None: xform = {'eval': ExpressionASTTransformer}.get(mode, TemplateASTTransformer) - tree = xform().visit(node) + tree = xform(secure).visit(node) if isinstance(filename, unicode): # unicode file names not allowed for code objects filename = filename.encode('utf-8', 'replace') @@ -415,6 +484,19 @@ BUILTINS = __builtin__.__dict__.copy() BUILTINS.update({'Markup': Markup, 'Undefined': Undefined}) + +# XXX: if we weaken the rule for global name resultion so that leading +# underscores are valid we have to add __import__ here. +UNSAFE_NAMES = ['file', 'open', 'eval', 'locals', 'globals', 'vars', + 'help', 'quit', 'exit', 'input', 'raw_input', 'setattr', + 'delattr', 'reload', 'compile', 'range', 'type'] + +# XXX: provide a secure range function +SECURE_BUILTINS = BUILTINS.copy() +for _unsafe_name in UNSAFE_NAMES: + del SECURE_BUILTINS[_unsafe_name] +del _unsafe_name + CONSTANTS = frozenset(['False', 'True', 'None', 'NotImplemented', 'Ellipsis']) @@ -425,6 +507,9 @@ altered or replaced in some way. """ + def __init__(self, secure): + self.secure = secure + def visit(self, node): if node is None: return None @@ -658,7 +743,8 @@ for code embedded in templates. """ - def __init__(self): + def __init__(self, secure): + ASTTransformer.__init__(self, secure) self.locals = [CONSTANTS] def visitConst(self, node): @@ -745,6 +831,22 @@ node = ast.CallFunc(ast.Name('_lookup_name'), func_args) return node + def visitGetattr(self, node): + if self.secure: + return ast.CallFunc(ast.Name('_lookup_attr'), [ + ast.Name('data'), self.visit(node.expr), + ast.Const(node.attrname) + ]) + return ASTTransformer.visitGetattr(self, node) + + def visitSubscript(self, node): + if self.secure: + return ast.CallFunc(ast.Name('_lookup_item'), [ + ast.Name('data'), self.visit(node.expr), + ast.Tuple([self.visit(sub) for sub in node.subs]) + ]) + return ASTTransformer.visitSubscript(self, node) + class ExpressionASTTransformer(TemplateASTTransformer): """Concrete AST transformer that implements the AST transformations needed