# HG changeset patch # User aronacher # Date 1190815630 0 # Node ID 25d0368491acfacf2fdcf178b9f73703b9ebf5b7 # Parent 05f5ce5d830d2c8d5220653fa25788b95b2ae082 first implementaiton of a secure genshi diff --git a/genshi/template/base.py b/genshi/template/base.py --- a/genshi/template/base.py +++ b/genshi/template/base.py @@ -304,7 +304,8 @@ _number_conv = unicode # function used to convert numbers to event data def __init__(self, source, basedir=None, filename=None, loader=None, - encoding=None, lookup='strict', allow_exec=True): + encoding=None, lookup='strict', allow_exec=True, + secure=False): """Initialize a template from either a string, a file-like object, or an already parsed markup stream. @@ -323,6 +324,10 @@ default), "lenient", or a custom lookup class :param allow_exec: whether Python code blocks in templates should be allowed + :param secure: whether genshi should evaluate the template in safe + mode. See the documentation on the sandbox features + for more details. In secure mode allow_exec is + automatically disabled. :note: Changed in 0.5: Added the `allow_exec` argument """ @@ -334,7 +339,8 @@ self.filepath = filename self.loader = loader self.lookup = lookup - self.allow_exec = allow_exec + self.allow_exec = not secure and allow_exec + self.secure = secure self.filters = [self._flatten, self._eval, self._exec] if loader: diff --git a/genshi/template/directives.py b/genshi/template/directives.py --- a/genshi/template/directives.py +++ b/genshi/template/directives.py @@ -110,7 +110,8 @@ """ try: return expr and Expression(expr, template.filepath, lineno, - lookup=template.lookup) or None + lookup=template.lookup, + secure=template.secure) or None except SyntaxError, err: err.msg += ' in expression "%s" of "%s" directive' % (expr, cls.tagname) diff --git a/genshi/template/eval.py b/genshi/template/eval.py --- 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 diff --git a/genshi/template/markup.py b/genshi/template/markup.py --- a/genshi/template/markup.py +++ b/genshi/template/markup.py @@ -61,10 +61,11 @@ _number_conv = Markup def __init__(self, source, basedir=None, filename=None, loader=None, - encoding=None, lookup='strict', allow_exec=True): + encoding=None, lookup='strict', allow_exec=True, + secure=False): Template.__init__(self, source, basedir=basedir, filename=filename, loader=loader, encoding=encoding, lookup=lookup, - allow_exec=allow_exec) + allow_exec=allow_exec, secure=secure) # Make sure the include filter comes after the match filter if loader: self.filters.remove(self._include) diff --git a/genshi/template/text.py b/genshi/template/text.py --- a/genshi/template/text.py +++ b/genshi/template/text.py @@ -131,10 +131,11 @@ def __init__(self, source, basedir=None, filename=None, loader=None, encoding=None, lookup='strict', allow_exec=False, - delims=('{%', '%}', '{#', '#}')): + secure=False, delims=('{%', '%}', '{#', '#}')): self.delimiters = delims Template.__init__(self, source, basedir=basedir, filename=filename, - loader=loader, encoding=encoding, lookup=lookup) + loader=loader, encoding=encoding, lookup=lookup, + secure=secure) def _get_delims(self): return self._delims