Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 57 additions & 63 deletions src/RestrictedPython/Eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,40 @@
##############################################################################
"""Restricted Python Expressions."""

from RestrictedPython.RCompile import compile_restricted_eval
from string import strip
from string import translate
from ._compat import IS_PY2
from .compile import compile_restricted_eval

import string
import ast


nltosp = string.maketrans('\r\n', ' ')
if IS_PY2:
from string import maketrans
else:
maketrans = str.maketrans

default_guarded_getattr = getattr # No restrictions.

nltosp = maketrans('\r\n', ' ')

# No restrictions.
default_guarded_getattr = getattr


def default_guarded_getitem(ob, index):
# No restrictions.
return ob[index]


PROFILE = 0


class RestrictionCapableEval(object):
"""A base class for restricted code."""

globals = {'__builtins__': None}
rcode = None # restricted
ucode = None # unrestricted
# restricted
rcode = None

# unrestricted
ucode = None

# Names used by the expression
used = None

def __init__(self, expr):
Expand All @@ -47,74 +55,60 @@ def __init__(self, expr):

expr -- a string containing the expression to be evaluated.
"""
expr = strip(expr)
expr = expr.strip()
self.__name__ = expr
expr = translate(expr, nltosp)
expr = expr.translate(nltosp)
self.expr = expr
self.prepUnrestrictedCode() # Catch syntax errors.
# Catch syntax errors.
self.prepUnrestrictedCode()

def prepRestrictedCode(self):
if self.rcode is None:
if PROFILE:
from time import clock
start = clock()
co, err, warn, used = compile_restricted_eval(
self.expr, '<string>')
if PROFILE:
end = clock()
print('prepRestrictedCode: %d ms for %s' % (
(end - start) * 1000, repr(self.expr)))
if err:
raise SyntaxError(err[0])
self.used = tuple(used.keys())
self.rcode = co
result = compile_restricted_eval(self.expr, '<string>')
if result.errors:
raise SyntaxError(result.errors[0])
self.used = tuple(result.used_names)
self.rcode = result.code

def prepUnrestrictedCode(self):
if self.ucode is None:
# Use the standard compiler.
co = compile(self.expr, '<string>', 'eval')
exp_node = compile(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is prepUnrestrictedCode actually still needed?
I think getting SyntaxErrors and used names can be done by compile_restricted_eval.
Or do I miss something here?
(Maybe this can be delayed after the release to stay backwards compatible to see if some code uses this method or the ucode attribute.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, the only justification for this method is the ucode attribute.
I did a quick search and found one user of it: https://github.com/zopefoundation/DocumentTemplate/blob/6b526d4b76f5496de5049f365a09ab712751c532/src/DocumentTemplate/DT_Util.py#L212

My suggestion would be to keep this method and ucode. To stay backwards compatible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

self.expr,
'<string>',
'eval',
ast.PyCF_ONLY_AST)

co = compile(exp_node, '<string>', 'eval')

# Examine the ast to discover which names the expression needs.
if self.used is None:
# Examine the code object, discovering which names
# the expression needs.
names = list(co.co_names)
used = {}
i = 0
code = co.co_code
l = len(code)
LOAD_NAME = 101
HAVE_ARGUMENT = 90
while(i < l):
c = ord(code[i])
if c == LOAD_NAME:
name = names[ord(code[i + 1]) + 256 * ord(code[i + 2])]
used[name] = 1
i = i + 3
elif c >= HAVE_ARGUMENT:
i = i + 3
else:
i = i + 1
self.used = tuple(used.keys())
used = set()
for node in ast.walk(exp_node):
if isinstance(node, ast.Name):
if isinstance(node.ctx, ast.Load):
used.add(node.id)

self.used = tuple(used)

self.ucode = co

def eval(self, mapping):
# This default implementation is probably not very useful. :-(
# This is meant to be overridden.
self.prepRestrictedCode()
code = self.rcode
d = {'_getattr_': default_guarded_getattr,
'_getitem_': default_guarded_getitem}
d.update(self.globals)
has_key = d.has_key

global_scope = {
'_getattr_': default_guarded_getattr,
'_getitem_': default_guarded_getitem
}

global_scope.update(self.globals)

for name in self.used:
try:
if not has_key(name):
d[name] = mapping[name]
except KeyError:
# Swallow KeyErrors since the expression
# might not actually need the name. If it
# does need the name, a NameError will occur.
pass
return eval(code, d)
if (name not in global_scope) and (name in mapping):
global_scope[name] = mapping[name]

return eval(self.rcode, global_scope)

def __call__(self, **kw):
return self.eval(kw)
12 changes: 0 additions & 12 deletions src/RestrictedPython/tests/testRestrictions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
# here instead.

from RestrictedPython import PrintCollector
from RestrictedPython.Eval import RestrictionCapableEval
from RestrictedPython.RCompile import compile_restricted
from RestrictedPython.RCompile import RFunction
from RestrictedPython.RCompile import RModule
Expand Down Expand Up @@ -307,17 +306,6 @@ def test_NestedScopes1(self):
res = self.execFunc('nested_scopes_1')
self.assertEqual(res, 2)

def test_UnrestrictedEval(self):
expr = RestrictionCapableEval("{'a':[m.pop()]}['a'] + [m[0]]")
v = [12, 34]
expect = v[:]
expect.reverse()
res = expr.eval({'m': v})
self.assertEqual(res, expect)
v = [12, 34]
res = expr(m=v)
self.assertEqual(res, expect)

def test_StackSize(self):
for k, rfunc in rmodule.items():
if not k.startswith('_') and hasattr(rfunc, 'func_code'):
Expand Down
16 changes: 9 additions & 7 deletions src/RestrictedPython/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,13 @@ def __init__(self, errors=None, warnings=None, used_names=None):
super(RestrictingNodeTransformer, self).__init__()
self.errors = [] if errors is None else errors
self.warnings = [] if warnings is None else warnings
self.used_names = [] if used_names is None else used_names

# All the variables used by the incoming source.
# Internal names/variables, like the ones from 'gen_tmp_name', don't
# have to be added.
# 'used_names' is for example needed by 'RestrictionCapableEval' to
# know wich names it has to supply when calling the final code.
self.used_names = {} if used_names is None else used_names

# Global counter to construct temporary variable names.
self._tmp_idx = 0
Expand All @@ -116,12 +122,6 @@ def warn(self, node, info):
self.warnings.append(
'Line {lineno}: {info}'.format(lineno=lineno, info=info))

def use_name(self, node, info):
"""Record a security error discovered during transformation."""
lineno = getattr(node, 'lineno', None)
self.used_names.append(
'Line {lineno}: {info}'.format(lineno=lineno, info=info))

def guard_iter(self, node):
"""
Converts:
Expand Down Expand Up @@ -585,6 +585,8 @@ def visit_Name(self, node):
copy_locations(new_node, node)
return new_node

self.used_names[node.id] = True

self.check_name(node, node.id)
return node

Expand Down
8 changes: 8 additions & 0 deletions tests/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,11 @@ def test_compile__compile_restricted_eval__1(c_eval):
def test_compile__compile_restricted_eval__2(e_eval):
"""It compiles code as an Expression."""
assert e_eval('4 * 6') == 24


@pytest.mark.parametrize(*c_eval)
def test_compile__compile_restricted_eval__used_names(c_eval):
result = c_eval("a + b + func(x)")
assert result.errors == ()
assert result.warnings == []
assert result.used_names == {'a': True, 'b': True, 'x': True, 'func': True}
42 changes: 42 additions & 0 deletions tests/test_eval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from RestrictedPython.Eval import RestrictionCapableEval

import pytest


exp = """
{'a':[m.pop()]}['a'] \
+ [m[0]]
"""


def test_init():
ob = RestrictionCapableEval(exp)

assert ob.expr == "{'a':[m.pop()]}['a'] + [m[0]]"
assert ob.used == ('m', )
assert ob.ucode is not None
assert ob.rcode is None


def test_init_with_syntax_error():
with pytest.raises(SyntaxError):
RestrictionCapableEval("if:")


def test_prepRestrictedCode():
ob = RestrictionCapableEval(exp)
ob.prepRestrictedCode()
assert ob.used == ('m', )
assert ob.rcode is not None


def test_call():
ob = RestrictionCapableEval(exp)
ret = ob(m=[1, 2])
assert ret == [2, 1]


def test_eval():
ob = RestrictionCapableEval(exp)
ret = ob.eval({'m': [1, 2]})
assert ret == [2, 1]
6 changes: 1 addition & 5 deletions tests/transformer/test_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,7 @@ def test_transformer__RestrictingNodeTransformer__visit_Call__1(c_exec):
loc = {}
exec(result.code, {}, loc)
assert loc['a'] == 3
if c_exec is RestrictedPython.compile.compile_restricted_exec:
# The new version not yet supports `used_names`:
assert result.used_names == {}
else:
assert result.used_names == {'max': True}
assert result.used_names == {'max': True}


YIELD = """\
Expand Down