[Zope-Checkins] CVS: Zope/lib/python/RestrictedPython - Guards.py:1.8.6.2 Limits.py:1.4.6.1 RCompile.py:1.3.6.1 RCompile_2_1.py:1.3.6.1 RestrictionMutator.py:1.10.6.2 SelectCompiler.py:1.4.6.1

Tres Seaver tseaver at zope.com
Thu Jan 8 15:12:42 EST 2004


Update of /cvs-repository/Zope/lib/python/RestrictedPython
In directory cvs.zope.org:/tmp/cvs-serv29583/lib/python/RestrictedPython

Modified Files:
      Tag: Zope-2_6-branch
	Guards.py Limits.py RCompile.py RCompile_2_1.py 
	RestrictionMutator.py SelectCompiler.py 
Log Message:


  - Enforce new restrictions on untrusted code, identified during
    the December 2003 security audit.  These issues affect sites
    that allow untrusted users to write Python Scripts, Page Templates,
    and DTML:

    o Iteration over sequences could in some cases fail to check access 
      to an object obtained from the sequence. Subsequent checks (such 
      as for attributes access) of such an object would still be 
      performed, but it should not have been possible to obtain the 
      object in the first place.

    o List and dictionary instance methods such as the get method of 
      dictionary objects were not security aware and could return an 
      object without checking access to that object. Subsequent checks 
      (such as for attributes access) of such an object would still be 
      performed, but it should not have been possible to obtain the 
      object in the first place.

    o Use of 'import as. in Python scripts could potentially rebind 
      names in ways that could be used to avoid appropriate security 
      checks.

    o A number of newer built-ins (min, max, enumerate, iter, sum)
      were either unavailable in untrusted code or did not perform
      adequate security checking.

    o Unpacking via function calls, variable assignment, exception 
      variables and other contexts did not perform adequate security 
      checks, potentially allowing access to objects that should have 
      been protected.

    o DTMLMethods with proxy rights could incorrectly transfer those 
      rights via acquisition when traversing to a parent object.



=== Zope/lib/python/RestrictedPython/Guards.py 1.8.6.1 => 1.8.6.2 ===
--- Zope/lib/python/RestrictedPython/Guards.py:1.8.6.1	Wed Jun 11 13:28:40 2003
+++ Zope/lib/python/RestrictedPython/Guards.py	Thu Jan  8 15:12:10 2004
@@ -14,52 +14,78 @@
 
 __version__='$Revision$'[11:-2]
 
-import new
-
-safe_builtins = {}
-for name in ('None', 'abs', 'chr', 'divmod', 'float', 'hash', 'hex', 'int',
-             'len', 'max', 'min', 'oct', 'ord', 'round', 'str', 'pow',
-             'apply', 'callable', 'cmp', 'complex', 'isinstance',
-             'issubclass', 'long', 'repr', 'range', 'list', 'tuple',
-             'unichr', 'unicode',
-             'Exception',
-             'ArithmeticError', 'AssertionError', 'AttributeError',
-             'EOFError', 'EnvironmentError', 'FloatingPointError',
-             'IOError', 'ImportError', 'IndexError', 'KeyError',
-             'LookupError', 'NameError', 'OSError', 'OverflowError',
-             'RuntimeError', 'StandardError', 'SyntaxError',
-             'TypeError', 'UnicodeError', 'ValueError', 'ZeroDivisionError',):
-    safe_builtins[name] = __builtins__[name]
+import exceptions
 
+# This tiny set of safe builtins is extended by users of the module.
+# AccessControl.ZopeGuards contains a large set of wrappers for builtins.
+# DocumentTemplate.DT_UTil contains a few.
 
-def _full_read_guard(g_attr, g_item):
-    # Nested scope abuse!
-    # The arguments are used by class Wrapper
-    # safetype variable is used by guard()
-    safetype = {type(()): 1, type([]): 1, type({}): 1, type(''): 1}.has_key
-    def guard(ob, write=None, safetype=safetype):
-        # Don't bother wrapping simple types, or objects that claim to
-        # handle their own read security.
-        if safetype(type(ob)) or getattr(ob, '_guarded_reads', 0):
-            return ob
-        # ob is shared by class Wrapper, so the class instance wraps it.
-        class Wrapper:
-            def __len__(self):
-                # Required for slices with negative bounds
-                return len(ob)
-            def __getattr__(self, name):
-                return g_attr(ob, name)
-            def __getitem__(self, i):
-                # Must handle both item and slice access.
-                return g_item(ob, i)
-            # Optional, for combined read/write guard
-            def __setitem__(self, index, val):
-                write(ob)[index] = val
-            def __setattr__(self, attr, val):
-                setattr(write(ob), attr, val)
-        return Wrapper()
-    return guard
+safe_builtins = {}
 
+for name in ['False', 'None', 'True', 'abs', 'basestring', 'bool', 'callable',
+             'chr', 'cmp', 'complex', 'divmod', 'float', 'hash',
+             'hex', 'id', 'int', 'isinstance', 'issubclass', 'len',
+             'long', 'oct', 'ord', 'pow', 'range', 'repr', 'round',
+             'str', 'tuple', 'unichr', 'unicode', 'xrange', 'zip']:
+    if __builtins__.has_key(name):
+        safe_builtins[name] = __builtins__[name]
+
+# Wrappers provided by this module:
+# delattr
+# setattr
+
+# Wrappers provided by ZopeGuards:
+# __import__
+# apply
+# dict
+# enumerate
+# filter
+# getattr
+# hasattr
+# iter
+# list
+# map
+# max
+# min
+# sum
+
+# Builtins that are intentionally disabled
+# compile   - don't let them produce new code
+# dir       - a general purpose introspector, probably hard to wrap
+# execfile  - no direct I/O
+# file      - no direct I/O
+# globals   - uncontrolled namespace access
+# input     - no direct I/O
+# locals    - uncontrolled namespace access
+# open      - no direct I/O
+# raw_input - no direct I/O
+# vars      - uncontrolled namespace access
+
+# There are several strings that describe Python.  I think there's no
+# point to including these, although they are obviously safe:
+# copyright, credits, exit, help, license, quit
+
+# Not provided anywhere.  Do something about these?  Several are
+# related to new-style classes, which we are too scared of to support
+# <0.3 wink>.  coerce, buffer, and reload are esoteric enough that no
+# one should care.
+
+# buffer
+# classmethod
+# coerce
+# eval
+# intern
+# object
+# property
+# reload
+# slice
+# staticmethod
+# super
+# type
+
+for name in dir(exceptions):
+    if name[0] != "_":
+        safe_builtins[name] = getattr(exceptions, name)
 
 def _write_wrapper():
     # Construct the write wrapper class
@@ -93,7 +119,7 @@
     # safetype and Wrapper variables are used by guard()
     safetype = {type([]): 1, type({}): 1}.has_key
     Wrapper = _write_wrapper()
-    def guard(ob, safetype=safetype, Wrapper=Wrapper):
+    def guard(ob):
         # Don't bother wrapping simple types, or objects that claim to
         # handle their own write security.
         if safetype(type(ob)) or hasattr(ob, '_guarded_writes'):


=== Zope/lib/python/RestrictedPython/Limits.py 1.4 => 1.4.6.1 ===
--- Zope/lib/python/RestrictedPython/Limits.py:1.4	Wed Aug 14 17:44:31 2002
+++ Zope/lib/python/RestrictedPython/Limits.py	Thu Jan  8 15:12:10 2004
@@ -13,6 +13,8 @@
 
 __version__='$Revision$'[11:-2]
 
+from types import StringType
+
 limited_builtins = {}
 
 def limited_range(iFirst, *args):
@@ -34,13 +36,13 @@
 limited_builtins['range'] = limited_range
 
 def limited_list(seq):
-    if type(seq) is type(''):
+    if isinstance(seq, StringType):
         raise TypeError, 'cannot convert string to list'
     return list(seq)
 limited_builtins['list'] = limited_list
 
 def limited_tuple(seq):
-    if type(seq) is type(''):
+    if isinstance(seq, StringType):
         raise TypeError, 'cannot convert string to tuple'
     return tuple(seq)
 limited_builtins['tuple'] = limited_tuple


=== Zope/lib/python/RestrictedPython/RCompile.py 1.3 => 1.3.6.1 ===
--- Zope/lib/python/RestrictedPython/RCompile.py:1.3	Wed Aug 14 17:44:31 2002
+++ Zope/lib/python/RestrictedPython/RCompile.py	Thu Jan  8 15:12:10 2004
@@ -16,11 +16,9 @@
 
 __version__='$Revision$'[11:-2]
 
-
-from compiler import ast, parse, misc, syntax
+from compiler import ast, parse, misc, syntax, pycodegen
 from compiler.pycodegen import AbstractCompileMode, Expression, \
-     Interactive, Module
-from traceback import format_exception_only
+     Interactive, Module, ModuleCodeGenerator, FunctionCodeGenerator, findOp
 
 import MutatingWalker
 from RestrictionMutator import RestrictionMutator
@@ -39,8 +37,9 @@
         # Some other error occurred.
         raise
 
-
-class RestrictedCompileMode (AbstractCompileMode):
+class RestrictedCompileMode(AbstractCompileMode):
+    """Abstract base class for hooking up custom CodeGenerator."""
+    # See concrete subclasses below.
 
     def __init__(self, source, filename):
         self.rm = RestrictionMutator()
@@ -51,57 +50,33 @@
 
     def _get_tree(self):
         tree = self.parse()
-        rm = self.rm
-        MutatingWalker.walk(tree, rm)
-        if rm.errors:
-            raise SyntaxError, rm.errors[0]
+        MutatingWalker.walk(tree, self.rm)
+        if self.rm.errors:
+            raise SyntaxError, self.rm.errors[0]
         misc.set_filename(self.filename, tree)
         syntax.check(tree)
         return tree
 
+    def compile(self):
+        tree = self._get_tree()
+        gen = self.CodeGeneratorClass(tree)
+        self.code = gen.getCode()
 
-class RExpression(RestrictedCompileMode, Expression):
-    mode = "eval"
-    compile = Expression.compile
-
-
-class RInteractive(RestrictedCompileMode, Interactive):
-    mode = "single"
-    compile = Interactive.compile
-
-
-class RModule(RestrictedCompileMode, Module):
-    mode = "exec"
-    compile = Module.compile
-
-
-class RFunction(RModule):
-    """A restricted Python function built from parts.
-    """
-
-    def __init__(self, p, body, name, filename):
-        self.params = p
-        self.body = body
-        self.name = name
-        RModule.__init__(self, None, filename)
-
-    def parse(self):
-        # Parse the parameters and body, then combine them.
-        firstline = 'def f(%s): pass' % self.params
-        tree = niceParse(firstline, '<function parameters>', 'exec')
-        f = tree.node.nodes[0]
-        body_code = niceParse(self.body, self.filename, 'exec')
-        # Stitch the body code into the function.
-        f.code.nodes = body_code.node.nodes
-        f.name = self.name
-        # Look for a docstring.
-        stmt1 = f.code.nodes[0]
-        if (isinstance(stmt1, ast.Discard) and
-            isinstance(stmt1.expr, ast.Const) and
-            type(stmt1.expr.value) is type('')):
-            f.doc = stmt1.expr.value
-        return tree
 
+def get_mutated_ast(s, filename='<string>', mode='exec'):
+    '''Return an AST, mutated via RestrictionMutator.'''
+    result = {}
+    try:
+        result['tree'] = niceParse(s, filename, mode)
+    except Exception, e:
+        result['tree'] = None
+        result['err'] = e
+    else:
+        result['err'] = None
+    if not result['err']:
+        result['rm'] = rm = RestrictionMutator()
+        MutatingWalker.walk(result['tree'], rm)
+    return result
 
 def compileAndTuplize(gen):
     try:
@@ -110,31 +85,36 @@
         return None, (str(v),), gen.rm.warnings, gen.rm.used_names
     return gen.getCode(), (), gen.rm.warnings, gen.rm.used_names
 
-def compile_restricted_function(p, body, name, filename):
+def compile_restricted_function(p, body, name, filename, globalize=None):
     """Compiles a restricted code object for a function.
 
     The function can be reconstituted using the 'new' module:
 
     new.function(<code>, <globals>)
+
+    The globalize argument, if specified, is a list of variable names to be
+    treated as globals (code is generated as if each name in the list
+    appeared in a global statement at the top of the function).
     """
-    gen = RFunction(p, body, name, filename)
+    gen = RFunction(p, body, name, filename, globalize)
     return compileAndTuplize(gen)
 
-def compile_restricted_exec(s, filename='<string>'):
+def compile_restricted_exec(s, filename='<string>', nested_scopes=None):
     """Compiles a restricted code suite.
+    
+    Ignore 'nested_scopes' argument, which is here only for backward
+    compatilbility with the RCompile_2_1 version.
     """
     gen = RModule(s, filename)
     return compileAndTuplize(gen)
 
 def compile_restricted_eval(s, filename='<string>'):
-    """Compiles a restricted expression.
-    """
+    """Compiles a restricted expression."""
     gen = RExpression(s, filename)
     return compileAndTuplize(gen)
 
 def compile_restricted(source, filename, mode):
-    """Replacement for the builtin compile() function.
-    """
+    """Replacement for the builtin compile() function."""
     if mode == "single":
         gen = RInteractive(source, filename)
     elif mode == "exec":
@@ -146,3 +126,129 @@
                          "'eval' or 'single'")
     gen.compile()
     return gen.getCode()
+
+class RestrictedCodeGenerator:
+    """Mixin for CodeGenerator to replace UNPACK_SEQUENCE bytecodes.
+
+    The UNPACK_SEQUENCE opcode is not safe because it extracts
+    elements from a sequence without using a safe iterator or
+    making __getitem__ checks.
+
+    This code generator replaces use of UNPACK_SEQUENCE with calls to
+    a function that unpacks the sequence, performes the appropriate
+    security checks, and returns a simple list.
+    """
+
+    # Replace the standard code generator for assignments to tuples
+    # and lists.
+
+    def _gen_safe_unpack_sequence(self, num):
+        # We're at a place where UNPACK_SEQUENCE should be generated, to
+        # unpack num items.  That's a security hole, since it exposes
+        # individual items from an arbitrary iterable.  We don't remove
+        # the UNPACK_SEQUENCE, but instead insert a call to our _getiter_()
+        # wrapper first.  That applies security checks to each item as
+        # it's delivered.  codegen is (just) a bit messy because the
+        # iterable is already on the stack, so we have to do a stack swap
+        # to get things in the right order.
+        self.emit('LOAD_GLOBAL', '_getiter_')
+        self.emit('ROT_TWO')
+        self.emit('CALL_FUNCTION', 1)
+        self.emit('UNPACK_SEQUENCE', num)
+
+    def _visitAssSequence(self, node):
+        if findOp(node) != 'OP_DELETE':
+            self._gen_safe_unpack_sequence(len(node.nodes))
+        for child in node.nodes:
+            self.visit(child)
+
+    visitAssTuple = _visitAssSequence
+    visitAssList = _visitAssSequence
+
+    # Call to generate code for unpacking nested tuple arguments
+    # in function calls.
+
+    def unpackSequence(self, tup):
+        self._gen_safe_unpack_sequence(len(tup))
+        for elt in tup:
+            if isinstance(elt, tuple):
+                self.unpackSequence(elt)
+            else:
+                self._nameOp('STORE', elt)
+
+# A collection of code generators that adds the restricted mixin to
+# handle unpacking for all the different compilation modes.  They
+# are defined here (at the end) so that can refer to RestrictedCodeGenerator.
+
+class RestrictedFunctionCodeGenerator(RestrictedCodeGenerator,
+                                      pycodegen.FunctionCodeGenerator):
+    pass
+
+class RestrictedExpressionCodeGenerator(RestrictedCodeGenerator,
+                                        pycodegen.ExpressionCodeGenerator):
+    pass
+
+class RestrictedInteractiveCodeGenerator(RestrictedCodeGenerator,
+                                         pycodegen.InteractiveCodeGenerator):
+    pass
+
+class RestrictedModuleCodeGenerator(RestrictedCodeGenerator,
+                                    pycodegen.ModuleCodeGenerator):
+
+    def initClass(self):
+        ModuleCodeGenerator.initClass(self)
+        self.__class__.FunctionGen = RestrictedFunctionCodeGenerator
+
+
+# These subclasses work around the definition of stub compile and mode
+# attributes in the common base class AbstractCompileMode.  If it
+# didn't define new attributes, then the stub code inherited via
+# RestrictedCompileMode would override the real definitions in
+# Expression.
+
+class RExpression(RestrictedCompileMode, Expression):
+    mode = "eval"
+    CodeGeneratorClass = RestrictedExpressionCodeGenerator
+
+class RInteractive(RestrictedCompileMode, Interactive):
+    mode = "single"
+    CodeGeneratorClass = RestrictedInteractiveCodeGenerator
+
+class RModule(RestrictedCompileMode, Module):
+    mode = "exec"
+    CodeGeneratorClass = RestrictedModuleCodeGenerator
+
+class RFunction(RModule):
+    """A restricted Python function built from parts."""
+
+    CodeGeneratorClass = RestrictedModuleCodeGenerator
+
+    def __init__(self, p, body, name, filename, globals):
+        self.params = p
+        self.body = body
+        self.name = name
+        self.globals = globals or []
+        RModule.__init__(self, None, filename)
+
+    def parse(self):
+        # Parse the parameters and body, then combine them.
+        firstline = 'def f(%s): pass' % self.params
+        tree = niceParse(firstline, '<function parameters>', 'exec')
+        f = tree.node.nodes[0]
+        body_code = niceParse(self.body, self.filename, 'exec')
+        # Stitch the body code into the function.
+        f.code.nodes = body_code.node.nodes
+        f.name = self.name
+        # Look for a docstring.
+        stmt1 = f.code.nodes[0]
+        if (isinstance(stmt1, ast.Discard) and
+            isinstance(stmt1.expr, ast.Const) and
+            isinstance(stmt1.expr.value, str)):
+            f.doc = stmt1.expr.value
+        # The caller may specify that certain variables are globals
+        # so that they can be referenced before a local assignment.
+        # The only known example is the variables context, container,
+        # script, traverse_subpath in PythonScripts.
+        if self.globals:
+            f.code.nodes.insert(0, ast.Global(self.globals))
+        return tree


=== Zope/lib/python/RestrictedPython/RCompile_2_1.py 1.3 => 1.3.6.1 ===
--- Zope/lib/python/RestrictedPython/RCompile_2_1.py:1.3	Wed Aug 14 17:44:31 2002
+++ Zope/lib/python/RestrictedPython/RCompile_2_1.py	Thu Jan  8 15:12:10 2004
@@ -15,6 +15,7 @@
 
 import sys
 from traceback import format_exception_only
+from types import TupleType
 
 def getSyntaxError(source, mode):
     try:
@@ -43,6 +44,78 @@
 from RestrictionMutator import RestrictionMutator
 from compiler_2_1 import ast, visitor, pycodegen
 
+class RestrictedCodeGenerator:
+    """Mixin for CodeGenerator to replace UNPACK_SEQUENCE bytecodes.
+
+    The UNPACK_SEQUENCE opcode is not safe because it extracts
+    elements from a sequence without using a safe iterator or
+    making __getitem__ checks.
+
+    This code generator replaces use of UNPACK_SEQUENCE with calls to
+    a function that unpacks the sequence, performes the appropriate
+    security checks, and returns a simple list.
+    """
+
+    # Replace the standard code generator for assignments to tuples
+    # and lists.
+
+    def _gen_safe_unpack_sequence(self, num):
+        # We're at a place where UNPACK_SEQUENCE should be generated, to
+        # unpack num items.  That's a security hole, since it exposes
+        # individual items from an arbitrary iterable.  We don't remove
+        # the UNPACK_SEQUENCE, but instead insert a call to our _getiter_()
+        # wrapper first.  That applies security checks to each item as
+        # it's delivered.  codegen is (just) a bit messy because the
+        # iterable is already on the stack, so we have to do a stack swap
+        # to get things in the right order.
+        self.emit('LOAD_GLOBAL', '_getiter_')
+        self.emit('ROT_TWO')
+        self.emit('CALL_FUNCTION', 1)
+        self.emit('UNPACK_SEQUENCE', num)
+
+    def _visitAssSequence(self, node):
+        if pycodegen.findOp(node) != 'OP_DELETE':
+            self._gen_safe_unpack_sequence(len(node.nodes))
+        for child in node.nodes:
+            self.visit(child)
+
+    visitAssTuple = _visitAssSequence
+    visitAssList = _visitAssSequence
+
+    # Call to generate code for unpacking nested tuple arguments
+    # in function calls.
+
+    def unpackSequence(self, tup):
+        self._gen_safe_unpack_sequence(len(tup))
+        for elt in tup:
+            if isinstance(elt, TupleType):
+                self.unpackSequence(elt)
+            else:
+                self._nameOp('STORE', elt)
+
+# Create variants of the standard code generators that mixin the
+# RestrictedCodeGenerator.  The various classes must be hooked
+# together via an initClass() call.
+
+class RNestedFunctionCodeGenerator(RestrictedCodeGenerator,
+                                   pycodegen.NestedFunctionCodeGenerator):
+    pass
+
+class RNestedScopeModuleCodeGenerator(
+    RestrictedCodeGenerator,
+    pycodegen.NestedScopeModuleCodeGenerator):
+
+    def initClass(self):
+        pycodegen.NestedScopeMixin.initClass(self)
+        self.__class__.FunctionGen = RNestedFunctionCodeGenerator
+
+class RModuleCodeGenerator(RestrictedCodeGenerator,
+                           pycodegen.ModuleCodeGenerator):
+
+    def initClass(self):
+        pycodegen.LGBScopeMixin.initClass(self)
+        self.__class__.FunctionGen = RNestedFunctionCodeGenerator
+
 def compile_restricted_function(p, body, name, filename):
     '''Compile a restricted code object for a function.
 
@@ -74,23 +147,32 @@
     MutatingWalker.walk(tree, rm)
     if rm.errors:
         return None, rm.errors, rm.warnings, rm.used_names
-    gen = pycodegen.NestedScopeModuleCodeGenerator(filename)
+    gen = RNestedScopeModuleCodeGenerator(filename)
     visitor.walk(tree, gen)
     return gen.getCode(), (), rm.warnings, rm.used_names
 
+def get_mutated_ast(s, filename='<string>'):
+    '''Return an AST, mutated via RestrictionMutator.'''
+    result = {}
+    result['tree'], result['err'] = tryParsing(s, 'exec')
+    if not result['err']:
+        result['rm'] = rm = RestrictionMutator()
+        MutatingWalker.walk(result['tree'], rm)
+    return result
+
 def compile_restricted_exec(s, filename='<string>', nested_scopes=1):
     '''Compile a restricted code suite.'''
-    rm = RestrictionMutator()
-    tree, err = tryParsing(s, 'exec')
-    if err: return err
-    MutatingWalker.walk(tree, rm)
+    ast_info = get_mutated_ast(s, filename)
+    if ast_info['err']:
+        return ast_info['err']
+    rm = ast_info['rm']
     if rm.errors:
         return None, rm.errors, rm.warnings, rm.used_names
     if nested_scopes:
-        gen = pycodegen.NestedScopeModuleCodeGenerator(filename)
+        gen = RNestedScopeModuleCodeGenerator(filename)
     else:
-        gen = pycodegen.ModuleCodeGenerator(filename)
-    visitor.walk(tree, gen)
+        gen = RModuleCodeGenerator(filename)
+    visitor.walk(ast_info['tree'], gen)
     return gen.getCode(), (), rm.warnings, rm.used_names
 
 if 1:


=== Zope/lib/python/RestrictedPython/RestrictionMutator.py 1.10.6.1 => 1.10.6.2 ===
--- Zope/lib/python/RestrictedPython/RestrictionMutator.py:1.10.6.1	Wed Nov  5 19:41:28 2003
+++ Zope/lib/python/RestrictedPython/RestrictionMutator.py	Thu Jan  8 15:12:10 2004
@@ -10,13 +10,16 @@
 # FOR A PARTICULAR PURPOSE
 #
 ##############################################################################
-'''
+"""Modify AST to include security checks.
+
 RestrictionMutator modifies a tree produced by
 compiler.transformer.Transformer, restricting and enhancing the
 code in various ways before sending it to pycodegen.
-'''
-__version__='$Revision$'[11:-2]
 
+$Revision$
+"""
+
+from types import StringType
 from SelectCompiler import ast, parse, OP_ASSIGN, OP_DELETE, OP_APPLY
 
 # These utility functions allow us to generate AST subtrees without
@@ -31,55 +34,33 @@
             rmLineno(child)
 
 def stmtNode(txt):
-    '''Make a "clean" statement node'''
+    """Make a "clean" statement node"""
     node = parse(txt).node.nodes[0]
     rmLineno(node)
     return node
 
-def exprNode(txt):
-    '''Make a "clean" expression node'''
-    return stmtNode(txt).expr
-
-# There should be up to four objects in the global namespace.
-# If a wrapper function or print target is needed in a particular
-# module or function, it is obtained from one of these objects.
-# It is stored in a variable with the same name as the global
-# object, but without a single trailing underscore.  This variable is
-# local, and therefore efficient to access, in function scopes.
-_print_target_name = ast.Name('_print')
-_getattr_name = ast.Name('_getattr')
-_getattr_name_expr = ast.Name('_getattr_')
-_getitem_name = ast.Name('_getitem')
-_getitem_name_expr = ast.Name('_getitem_')
-_write_guard_name = ast.Name('_write')
+# The security checks are performed by a set of six functions that
+# must be provided by the restricted environment.
+
+_apply_name = ast.Name("_apply_")
+_getattr_name = ast.Name("_getattr_")
+_getitem_name = ast.Name("_getitem_")
+_getiter_name = ast.Name("_getiter_")
+_print_target_name = ast.Name("_print")
+_write_name = ast.Name("_write_")
 
 # Constants.
 _None_const = ast.Const(None)
 _write_const = ast.Const('write')
 
-# Example prep code:
-#
-# global _getattr_
-# _getattr = _getattr_
-_prep_code = {}
-for _n in ('getattr', 'getitem', 'write', 'print'):
-    _prep_code[_n] = [ast.Global(['_%s_' % _n]),
-                      stmtNode('_%s = _%s_' % (_n, _n))]
-# Call the global _print instead of copying it.
-_prep_code['print'][1] = stmtNode('_print = _print_()')
-
-_printed_expr = exprNode('_print()')
+_printed_expr = stmtNode('_print()').expr
+_print_target_node = stmtNode("_print = _print_()")
 
 
 # Keep track of which restrictions have been applied in a given scope.
 class FuncInfo:
     _print_used = 0
     _printed_used = 0
-    _getattr_used = 0
-    _getitem_used = 0
-    _write_used = 0
-    _is_suite = 0  # True for modules and functions, false for expressions
-
 
 class RestrictionMutator:
 
@@ -88,8 +69,10 @@
         self.warnings = []
         self.errors = []
         self.used_names = {}
+        self.funcinfo = FuncInfo()
 
     def error(self, node, info):
+        """Records a security error discovered during compilation."""
         lineno = getattr(node, 'lineno', None)
         if lineno is not None and lineno > 0:
             self.errors.append('Line %d: %s' % (lineno, info))
@@ -97,11 +80,11 @@
             self.errors.append(info)
 
     def checkName(self, node, name):
-        if len(name) > 1 and name[0] == '_':
+        if name.startswith("_") and name != "_":
             # Note: "_" *is* allowed.
             self.error(node, '"%s" is an invalid variable name because'
                        ' it starts with "_"' % name)
-        if name == 'printed':
+        if name == "printed":
             self.error(node, '"printed" is a reserved name.')
 
     def checkAttrName(self, node):
@@ -109,40 +92,35 @@
         # and is thus essential regardless of the security policy,
         # unless some other solution is devised.
         name = node.attrname
-        if len(name) > 1 and name[0] == '_':
+        if name.startswith("_") and name != "_":
             # Note: "_" *is* allowed.
             self.error(node, '"%s" is an invalid attribute name '
                        'because it starts with "_".' % name)
 
     def prepBody(self, body):
-        """Appends prep code to the beginning of a code suite.
-        """
-        info = self.funcinfo
-        if info._print_used or info._printed_used:
+        """Insert code for print at the beginning of the code suite."""
+        if self.funcinfo._print_used or self.funcinfo._printed_used:
             # Add code at top for creating _print_target
-            body[0:0] = _prep_code['print']
-            if not info._printed_used:
+            body.insert(0, _print_target_node)
+            if not self.funcinfo._printed_used:
                 self.warnings.append(
                     "Prints, but never reads 'printed' variable.")
-            elif not info._print_used:
+            elif not self.funcinfo._print_used:
                 self.warnings.append(
                     "Doesn't print, but reads 'printed' variable.")
-        if info._getattr_used:
-            body[0:0] = _prep_code['getattr']
-        if info._getitem_used:
-            body[0:0] = _prep_code['getitem']
-        if info._write_used:
-            body[0:0] = _prep_code['write']
 
     def visitFunction(self, node, walker):
         self.checkName(node, node.name)
         for argname in node.argnames:
-            self.checkName(node, argname)
+            if isinstance(argname, StringType):
+                self.checkName(node, argname)
+            else:
+                for name in argname:
+                    self.checkName(node, name)
         walker.visitSequence(node.defaults)
 
         former_funcinfo = self.funcinfo
         self.funcinfo = FuncInfo()
-        self.funcinfo._is_suite = 1
         node = walker.defaultVisitNode(node, exclude=('defaults',))
         self.prepBody(node.code.nodes)
         self.funcinfo = former_funcinfo
@@ -159,7 +137,6 @@
         if node.dest is None:
             node.dest = _print_target_name
         else:
-            self.funcinfo._getattr_used = 1
             # Pre-validate access to the "write" attribute.
             # "print >> ob, x" becomes
             # "print >> (_getattr(ob, 'write') and ob), x"
@@ -178,11 +155,52 @@
         self.checkName(node, node.name)
         self.used_names[node.name] = 1
         return node
+  
+    def visitCallFunc(self, node, walker):
+        """Checks calls with *-args and **-args.
+
+        That's a way of spelling apply(), and needs to use our safe
+        _apply_ instead.
+        """
+        walked = walker.defaultVisitNode(node)
+        if node.star_args is None and node.dstar_args is None:
+            # This is not an extended function call
+            return walked
+        # Otherwise transform foo(a, b, c, d=e, f=g, *args, **kws) into a call
+        # of _apply_(foo, a, b, c, d=e, f=g, *args, **kws).  The interesting
+        # thing here is that _apply_() is defined with just *args and **kws,
+        # so it gets Python to collapse all the myriad ways to call functions
+        # into one manageable form.
+        #
+        # From there, _apply_() digs out the first argument of *args (it's the
+        # function to call), wraps args and kws in guarded accessors, then
+        # calls the function, returning the value.
+        # Transform foo(...) to _apply(foo, ...)
+        walked.args.insert(0, walked.node)
+        walked.node = _apply_name
+        return walked
 
     def visitAssName(self, node, walker):
         self.checkName(node, node.name)
         return node
 
+    def visitFor(self, node, walker):
+        # convert
+        #   for x in expr:
+        # to
+        #   for x in _getiter(expr):
+        #
+        # Note that visitListCompFor is the same thing.  Exactly the same
+        # transformation is needed to convert
+        #   [... for x in expr ...]
+        # to
+        #   [... for x in _getiter(expr) ...]
+        node = walker.defaultVisitNode(node)
+        node.list = ast.CallFunc(_getiter_name, [node.list])
+        return node
+
+    visitListCompFor = visitFor
+
     def visitGetattr(self, node, walker):
         self.checkAttrName(node)
         node = walker.defaultVisitNode(node)
@@ -191,14 +209,8 @@
             # We might support this later...
             self.error(node, 'Augmented assignment of '
                        'attributes is not allowed.')
-            #expr.append(_write_guard_name)
-            #self.funcinfo._write_used = 1
-        self.funcinfo._getattr_used = 1
-        if self.funcinfo._is_suite:
-            ga = _getattr_name
-        else:
-            ga = _getattr_name_expr
-        return ast.CallFunc(ga, [node.expr, ast.Const(node.attrname)])
+        return ast.CallFunc(_getattr_name,
+                            [node.expr, ast.Const(node.attrname)])
 
     def visitSubscript(self, node, walker):
         node = walker.defaultVisitNode(node)
@@ -209,9 +221,6 @@
                 # We might support this later...
                 self.error(node, 'Augmented assignment of '
                            'object items and slices is not allowed.')
-                #expr.append(_write_guard_name)
-                #self.funcinfo._write_used = 1
-            self.funcinfo._getitem_used = 1
             if hasattr(node, 'subs'):
                 # Subscript.
                 subs = node.subs
@@ -231,15 +240,10 @@
                 if upper is None:
                     upper = _None_const
                 subs = ast.Sliceobj([lower, upper])
-            if self.funcinfo._is_suite:
-                gi = _getitem_name
-            else:
-                gi = _getitem_name_expr
-            return ast.CallFunc(gi, [node.expr, subs])
+            return ast.CallFunc(_getitem_name, [node.expr, subs])
         elif node.flags in (OP_DELETE, OP_ASSIGN):
             # set or remove subscript or slice
-            node.expr = ast.CallFunc(_write_guard_name, [node.expr])
-            self.funcinfo._write_used = 1
+            node.expr = ast.CallFunc(_write_name, [node.expr])
         return node
 
     visitSlice = visitSubscript
@@ -247,8 +251,7 @@
     def visitAssAttr(self, node, walker):
         self.checkAttrName(node)
         node = walker.defaultVisitNode(node)
-        node.expr = ast.CallFunc(_write_guard_name, [node.expr])
-        self.funcinfo._write_used = 1
+        node.expr = ast.CallFunc(_write_name, [node.expr])
         return node
 
     def visitExec(self, node, walker):
@@ -263,7 +266,6 @@
         return walker.defaultVisitNode(node)
 
     def visitModule(self, node, walker):
-        self.funcinfo._is_suite = 1
         node = walker.defaultVisitNode(node)
         self.prepBody(node.node.nodes)
         return node


=== Zope/lib/python/RestrictedPython/SelectCompiler.py 1.4 => 1.4.6.1 ===
--- Zope/lib/python/RestrictedPython/SelectCompiler.py:1.4	Wed Aug 14 17:44:31 2002
+++ Zope/lib/python/RestrictedPython/SelectCompiler.py	Thu Jan  8 15:12:10 2004
@@ -27,7 +27,8 @@
          compile_restricted, \
          compile_restricted_function, \
          compile_restricted_exec, \
-         compile_restricted_eval
+         compile_restricted_eval, \
+         get_mutated_ast
 else:
     # Use the compiler from the standard library.
     import compiler
@@ -39,4 +40,5 @@
          compile_restricted, \
          compile_restricted_function, \
          compile_restricted_exec, \
-         compile_restricted_eval
+         compile_restricted_eval, \
+         get_mutated_ast




More information about the Zope-Checkins mailing list