Skip to content

Commit 079fb03

Browse files
committed
POC allowed_attrs
1 parent 44bb1a9 commit 079fb03

File tree

2 files changed

+181
-3
lines changed

2 files changed

+181
-3
lines changed

simpleeval.py

+156-3
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,147 @@
144144
# PyInstaller environment doesn't include this module.
145145
DISALLOW_FUNCTIONS.add(help)
146146

147+
# Opt-in type safety experiment. Will be opt-out in 2.x
148+
149+
BASIC_ALLOWED_ATTRS = {
150+
int: {
151+
"as_integer_ratio",
152+
"bit_length",
153+
"conjugate",
154+
"denominator",
155+
"from_bytes",
156+
"imag",
157+
"numerator",
158+
"real",
159+
"to_bytes",
160+
},
161+
float: {
162+
"as_integer_ratio",
163+
"conjugate",
164+
"fromhex",
165+
"hex",
166+
"imag",
167+
"is_integer",
168+
"real",
169+
},
170+
str: {
171+
"capitalize",
172+
"casefold",
173+
"center",
174+
"count",
175+
"encode",
176+
"endswith",
177+
"expandtabs",
178+
"find",
179+
"format",
180+
"format_map",
181+
"index",
182+
"isalnum",
183+
"isalpha",
184+
"isascii",
185+
"isdecimal",
186+
"isdigit",
187+
"isidentifier",
188+
"islower",
189+
"isnumeric",
190+
"isprintable",
191+
"isspace",
192+
"istitle",
193+
"isupper",
194+
"join",
195+
"ljust",
196+
"lower",
197+
"lstrip",
198+
"maketrans",
199+
"partition",
200+
"removeprefix",
201+
"removesuffix",
202+
"replace",
203+
"rfind",
204+
"rindex",
205+
"rjust",
206+
"rpartition",
207+
"rsplit",
208+
"rstrip",
209+
"split",
210+
"splitlines",
211+
"startswith",
212+
"strip",
213+
"swapcase",
214+
"title",
215+
"translate",
216+
"upper",
217+
"zfill",
218+
},
219+
bool: {
220+
"as_integer_ratio",
221+
"bit_length",
222+
"conjugate",
223+
"denominator",
224+
"from_bytes",
225+
"imag",
226+
"numerator",
227+
"real",
228+
"to_bytes",
229+
},
230+
None: {},
231+
dict: {
232+
"clear",
233+
"copy",
234+
"fromkeys",
235+
"get",
236+
"items",
237+
"keys",
238+
"pop",
239+
"popitem",
240+
"setdefault",
241+
"update",
242+
"values",
243+
},
244+
list: {
245+
"pop",
246+
"append",
247+
"index",
248+
"reverse",
249+
"count",
250+
"sort",
251+
"copy",
252+
"extend",
253+
"clear",
254+
"insert",
255+
"remove",
256+
},
257+
set: {
258+
"pop",
259+
"intersection_update",
260+
"intersection",
261+
"issubset",
262+
"symmetric_difference_update",
263+
"discard",
264+
"isdisjoint",
265+
"difference_update",
266+
"issuperset",
267+
"add",
268+
"copy",
269+
"union",
270+
"clear",
271+
"update",
272+
"symmetric_difference",
273+
"difference",
274+
"remove",
275+
},
276+
tuple: {"index", "count"},
277+
}
278+
147279

148280
########################################
149281
# Exceptions:
150282

151283

284+
class TypeNotSpecified(Exception):
285+
pass
286+
287+
152288
class InvalidExpression(Exception):
153289
"""Generic Exception"""
154290

@@ -346,7 +482,7 @@ class SimpleEval(object): # pylint: disable=too-few-public-methods
346482

347483
expr = ""
348484

349-
def __init__(self, operators=None, functions=None, names=None):
485+
def __init__(self, operators=None, functions=None, names=None, allowed_attrs=None):
350486
"""
351487
Create the evaluator instance. Set up valid operators (+,-, etc)
352488
functions (add, random, get_val, whatever) and names."""
@@ -361,6 +497,7 @@ def __init__(self, operators=None, functions=None, names=None):
361497
self.operators = operators
362498
self.functions = functions
363499
self.names = names
500+
self.allowed_attrs = allowed_attrs
364501

365502
self.nodes = {
366503
ast.Expr: self._eval_expr,
@@ -589,6 +726,17 @@ def _eval_subscript(self, node):
589726
return container[key]
590727

591728
def _eval_attribute(self, node):
729+
if self.allowed_attrs is not None:
730+
allowed_attrs = self.allowed_attrs.get(type(node.value.value), TypeNotSpecified)
731+
if allowed_attrs == TypeNotSpecified:
732+
raise FeatureNotAvailable(
733+
f"Sorry, attribute access not allowed on '{type(node.value.value)}'"
734+
)
735+
if node.attr not in allowed_attrs:
736+
raise FeatureNotAvailable(
737+
f"Sorry, '{node.attr}' access not allowed on '{type(node.value.value)}'"
738+
)
739+
592740
for prefix in DISALLOW_PREFIXES:
593741
if node.attr.startswith(prefix):
594742
raise FeatureNotAvailable(
@@ -764,7 +912,12 @@ def do_generator(gi=0):
764912
return to_return
765913

766914

767-
def simple_eval(expr, operators=None, functions=None, names=None):
915+
def simple_eval(expr, operators=None, functions=None, names=None, allowed_attrs=None):
768916
"""Simply evaluate an expresssion"""
769-
s = SimpleEval(operators=operators, functions=functions, names=names)
917+
s = SimpleEval(
918+
operators=operators,
919+
functions=functions,
920+
names=names,
921+
allowed_attrs=allowed_attrs,
922+
)
770923
return s.eval(expr)

test_simpleeval.py

+25
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import simpleeval
2121
from simpleeval import (
22+
BASIC_ALLOWED_ATTRS,
2223
AttributeDoesNotExist,
2324
EvalWithCompoundTypes,
2425
FeatureNotAvailable,
@@ -1351,5 +1352,29 @@ def test_no_operators(self):
13511352
s.eval("~ 2")
13521353

13531354

1355+
class TestAllowedAttributes(DRYTest):
1356+
def test_allowed_attrs_(self):
1357+
self.s.allowed_attrs = BASIC_ALLOWED_ATTRS
1358+
self.t("5 + 5", 10)
1359+
self.t('" hello ".strip()', "hello")
1360+
1361+
def test_breakout_via_generator(self):
1362+
# Thanks decorator-factory
1363+
class Foo:
1364+
def bar(self):
1365+
yield "Hello, world!"
1366+
1367+
# Test the genertor does work - also adds the `yield` to codecov...
1368+
assert list(Foo().bar()) == ["Hello, world!"]
1369+
1370+
evil = "foo.bar().gi_frame.f_globals['__builtins__'].exec('raise RuntimeError(\"Oh no\")')"
1371+
1372+
x = simpleeval.DISALLOW_METHODS
1373+
simpleeval.DISALLOW_METHODS = []
1374+
with self.assertRaises(FeatureNotAvailable):
1375+
simple_eval(evil, names={"foo": Foo()}, allowed_attrs=BASIC_ALLOWED_ATTRS)
1376+
simpleeval.DISALLOW_METHODS = x
1377+
1378+
13541379
if __name__ == "__main__": # pragma: no cover
13551380
unittest.main()

0 commit comments

Comments
 (0)