Skip to content

Commit fbf72d8

Browse files
Add strategy for safely evaluating builtins
1 parent d2112a2 commit fbf72d8

File tree

3 files changed

+155
-14
lines changed

3 files changed

+155
-14
lines changed

bpython/autocomplete.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from bpython.line import LinePart
4444
from bpython._py3compat import py3, try_decode
4545
from bpython.lazyre import LazyReCompile
46+
from bpython.simpleeval import safe_eval, EvaluationError
4647

4748
if not py3:
4849
from types import InstanceType, ClassType
@@ -648,20 +649,6 @@ def get_completer_bpython(cursor_offset, line, **kwargs):
648649
cursor_offset, line, **kwargs)
649650

650651

651-
class EvaluationError(Exception):
652-
"""Raised if an exception occurred in safe_eval."""
653-
654-
655-
def safe_eval(expr, namespace):
656-
"""Not all that safe, just catches some errors"""
657-
try:
658-
return eval(expr, namespace)
659-
except (NameError, AttributeError, SyntaxError):
660-
# If debugging safe_eval, raise this!
661-
# raise
662-
raise EvaluationError
663-
664-
665652
def _callable_postfix(value, word):
666653
"""rlcompleter's _callable_postfix done right."""
667654
with inspection.AttrCleaner(value):

bpython/simpleeval.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# encoding: utf-8
2+
"""simple evaluation of side-effect free code
3+
4+
In order to provide fancy completion, some code can be executed safely.
5+
6+
"""
7+
8+
from ast import *
9+
from six import string_types
10+
11+
class EvaluationError(Exception):
12+
"""Raised if an exception occurred in safe_eval."""
13+
14+
15+
def safe_eval(expr, namespace):
16+
"""Not all that safe, just catches some errors"""
17+
try:
18+
return eval(expr, namespace)
19+
except (NameError, AttributeError, SyntaxError):
20+
# If debugging safe_eval, raise this!
21+
# raise
22+
raise EvaluationError
23+
24+
def simple_eval(node_or_string, namespace={}):
25+
"""
26+
Safely evaluate an expression node or a string containing a Python
27+
expression. The string or node provided may only consist of:
28+
* the following Python literal structures: strings, numbers, tuples,
29+
lists, dicts, booleans, and None.
30+
* variable names causing lookups in the passed in namespace or builtins
31+
* getitem calls using the [] syntax on objects of the types above
32+
* getitem calls on subclasses of the above types if they do not override
33+
the __getitem__ method and do not override __getattr__ or __getattribute__
34+
(or maybe we'll try to clean those up?)
35+
36+
The optional namespace dict-like ought not to cause side effects on lookup
37+
"""
38+
if isinstance(node_or_string, string_types):
39+
node_or_string = parse(node_or_string, mode='eval')
40+
if isinstance(node_or_string, Expression):
41+
node_or_string = node_or_string.body
42+
def _convert(node):
43+
if isinstance(node, Str):
44+
return node.s
45+
elif isinstance(node, Num):
46+
return node.n
47+
elif isinstance(node, Tuple):
48+
return tuple(map(_convert, node.elts))
49+
elif isinstance(node, List):
50+
return list(map(_convert, node.elts))
51+
elif isinstance(node, Dict):
52+
return dict((_convert(k), _convert(v)) for k, v
53+
in zip(node.keys, node.values))
54+
elif isinstance(node, Name):
55+
try:
56+
return namespace[node.id]
57+
except KeyError:
58+
return __builtins__[node.id]
59+
elif isinstance(node, BinOp) and \
60+
isinstance(node.op, (Add, Sub)) and \
61+
isinstance(node.right, Num) and \
62+
isinstance(node.right.n, complex) and \
63+
isinstance(node.left, Num) and \
64+
isinstance(node.left.n, (int, long, float)):
65+
left = node.left.n
66+
right = node.right.n
67+
if isinstance(node.op, Add):
68+
return left + right
69+
else:
70+
return left - right
71+
elif isinstance(node, Subscript) and \
72+
isinstance(node.slice, Index):
73+
obj = _convert(node.value)
74+
index = _convert(node.slice.value)
75+
return safe_getitem(obj, index)
76+
77+
raise ValueError('malformed string')
78+
return _convert(node_or_string)
79+
80+
def safe_getitem(obj, index):
81+
if type(obj) in (list, tuple, dict, bytes) + string_types:
82+
return obj[index]
83+
raise ValueError('unsafe to lookup on object of type %s' % (type(obj), ))

bpython/test/test_simpleeval.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import ast
4+
5+
from bpython.simpleeval import simple_eval
6+
from bpython.test import unittest
7+
8+
9+
class TestInspection(unittest.TestCase):
10+
def assertMatchesStdlib(self, expr):
11+
self.assertEqual(ast.literal_eval(expr), simple_eval(expr))
12+
13+
def test_matches_stdlib(self):
14+
"""Should match the stdlib literal_eval if no names or indexing"""
15+
self.assertMatchesStdlib("[1]")
16+
self.assertMatchesStdlib("{(1,): [2,3,{}]}")
17+
18+
def test_indexing(self):
19+
"""Literals can be indexed into"""
20+
self.assertEqual(simple_eval('[1,2][0]'), 1)
21+
self.assertEqual(simple_eval('a', {'a':1}), 1)
22+
23+
def test_name_lookup(self):
24+
"""Names can be lookup up in a namespace"""
25+
self.assertEqual(simple_eval('a', {'a':1}), 1)
26+
self.assertEqual(simple_eval('map'), map)
27+
self.assertEqual(simple_eval('a[b]', {'a':{'c':1}, 'b':'c'}), 1)
28+
29+
def test_allow_name_lookup(self):
30+
"""Names can be lookup up in a namespace"""
31+
self.assertEqual(simple_eval('a', {'a':1}), 1)
32+
33+
def test_lookup_on_suspicious_types(self):
34+
class FakeDict(object):
35+
pass
36+
37+
with self.assertRaises(ValueError):
38+
simple_eval('a[1]', {'a': FakeDict()})
39+
40+
class TrickyDict(dict):
41+
def __getitem__(self, index):
42+
self.fail("doing key lookup isn't safe")
43+
44+
with self.assertRaises(ValueError):
45+
simple_eval('a[1]', {'a': TrickyDict()})
46+
47+
class SchrodingersDict(dict):
48+
def __getattribute__(inner_self, attr):
49+
self.fail("doing attribute lookup might have side effects")
50+
51+
with self.assertRaises(ValueError):
52+
simple_eval('a[1]', {'a': SchrodingersDict()})
53+
54+
class SchrodingersCatsDict(dict):
55+
def __getattr__(inner_self, attr):
56+
self.fail("doing attribute lookup might have side effects")
57+
58+
with self.assertRaises(ValueError):
59+
simple_eval('a[1]', {'a': SchrodingersDict()})
60+
61+
def test_function_calls_raise(self):
62+
with self.assertRaises(ValueError):
63+
simple_eval('1()')
64+
65+
def test_nonexistant_names_raise(self):
66+
with self.assertRaises(KeyError):
67+
simple_eval('a')
68+
69+
70+
if __name__ == '__main__':
71+
unittest.main()

0 commit comments

Comments
 (0)