Skip to content

Commit 8186abf

Browse files
committed
Convert MAX_VARS and MAX_VAR_MEMORY from class constants to parser attributes, and document as part of public API; add label comments in arith_tests.py, and move max_number_of_vars test to test_unit.py;
1 parent 6f8e6fd commit 8186abf

File tree

4 files changed

+40
-58
lines changed

4 files changed

+40
-58
lines changed

CHANGES

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ plusminus Change Log
5757
A customizable parser attribute maximum_formula_depth will limit the number
5858
of formula indirections. The default value is 12.
5959

60+
- An attack may try to define too many variables and crash an application
61+
by consuming excessive memory. A value to limit the number of variables and
62+
their respective memory usage was previously hard-coded. These are now
63+
part of the public API for parsers: max_number_of_vars (default = 1000)
64+
and max_var_memory (default = 10MB).
65+
6066

6167
0.2.0 -
6268

plusminus/plusminus.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,6 @@ class ArithmeticParser:
387387

388388
LEFT = pp.opAssoc.LEFT
389389
RIGHT = pp.opAssoc.RIGHT
390-
MAX_VARS = 1000
391-
MAX_VAR_MEMORY = 10 ** 6
392390

393391
def usage(self):
394392
import textwrap
@@ -539,6 +537,9 @@ def __repr__(self):
539537
return "{}({})".format(self.tokens[0], ", ".join(map(repr, self.tokens[1:])))
540538

541539
def __init__(self):
540+
self.max_number_of_vars = 1000
541+
self.max_var_memory = 10 ** 6
542+
542543
self._added_operator_specs = []
543544
self._added_function_specs = {}
544545
self._base_operators = (
@@ -951,7 +952,7 @@ def eval_and_store_value(tokens):
951952
var_name = lhs_name.name
952953
if (
953954
var_name not in assigned_vars
954-
and len(assigned_vars) >= self.MAX_VARS
955+
and len(assigned_vars) >= self.max_number_of_vars
955956
):
956957
raise Exception("too many variables defined")
957958
assigned_vars[var_name] = rval
@@ -1024,8 +1025,8 @@ def get_depth(formula_node):
10241025
assigned_vars = identifier_node_class._assigned_vars
10251026
if (
10261027
dest_var_name not in assigned_vars
1027-
and len(assigned_vars) >= self.MAX_VARS
1028-
or sum(sys.getsizeof(vv) for vv in assigned_vars.values()) > self.MAX_VAR_MEMORY
1028+
and len(assigned_vars) >= self.max_number_of_vars
1029+
or sum(sys.getsizeof(vv) for vv in assigned_vars.values()) > self.max_var_memory
10291030
):
10301031
raise Exception("too many variables defined")
10311032
assigned_vars[dest_var_name] = rval

test/arith_tests.py

Lines changed: 14 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,9 @@ def post_parse_evaluate(teststr, result):
244244
print('circle_area =', parser['circle_area'])
245245
print('circle_area =', parser.evaluate('circle_area'))
246246

247+
print("del parser['circle_radius']")
247248
del parser['circle_radius']
249+
248250
try:
249251
print('circle_area =', end=' ')
250252
print(parser.evaluate('circle_area'))
@@ -254,9 +256,9 @@ def post_parse_evaluate(teststr, result):
254256
print(parser.parse("6.02e24 * 100").evaluate())
255257

256258

257-
258259
parser = CombinatoricsArithmeticParser()
259260
parser.runTests("""\
261+
# CombinatoricsArithmeticParser
260262
3!
261263
-3!
262264
3!!
@@ -272,6 +274,7 @@ def post_parse_evaluate(teststr, result):
272274

273275
parser = BusinessArithmeticParser()
274276
parser.runTests("""\
277+
# BusinessArithmeticParser
275278
25%
276279
20 * 50%
277280
50% of 20
@@ -284,6 +287,7 @@ def post_parse_evaluate(teststr, result):
284287
""",
285288
postParse=post_parse_evaluate)
286289

290+
287291
from datetime import datetime
288292
class DateTimeArithmeticParser(ArithmeticParser):
289293
SECONDS_PER_MINUTE = 60
@@ -302,8 +306,10 @@ def customize(self):
302306
microsecond=0).timestamp())
303307
self.add_function('str', 1, lambda dt: str(datetime.fromtimestamp(dt)))
304308

309+
305310
parser = DateTimeArithmeticParser()
306311
parser.runTests("""\
312+
# DateTimeArithmeticParser
307313
now()
308314
str(now())
309315
str(today())
@@ -315,55 +321,10 @@ def customize(self):
315321

316322

317323
parser = DiceRollParser()
318-
parser.runTests("""
319-
d20
320-
3d6
321-
d20 + 3d4
322-
(3d6)/3
323-
""", postParse=post_parse_evaluate)
324-
325-
326-
print()
327-
328-
329-
# override max number of variables
330-
class restore:
331-
"""
332-
Context manager for restoring an object's attributes back the way they were if they were
333-
changed or deleted, or to remove any attributes that were added.
334-
"""
335-
def __init__(self, obj, *attr_names):
336-
self._obj = obj
337-
self._attrs = attr_names
338-
if not self._attrs:
339-
self._attrs = [name for name in vars(obj) if name not in ('__dict__', '__slots__')]
340-
self._no_attr_value = object()
341-
self._save_values = {}
342-
343-
def __enter__(self):
344-
for attr in self._attrs:
345-
self._save_values[attr] = getattr(self._obj, attr, self._no_attr_value)
346-
return self
347-
348-
def __exit__(self, *args):
349-
for attr in self._attrs:
350-
save_value = self._save_values[attr]
351-
if save_value is not self._no_attr_value:
352-
if getattr(self._obj, attr, self._no_attr_value) != save_value:
353-
print("reset", attr, "to", save_value)
354-
setattr(self._obj, attr, save_value)
355-
else:
356-
if hasattr(self._obj, attr):
357-
delattr(self._obj, attr)
358-
359-
360-
print('test defining too many vars (set max to 20)')
361-
with restore(ArithmeticParser):
362-
ArithmeticParser.MAX_VARS = 20
363-
parser = ArithmeticParser()
364-
try:
365-
for i in range(1000):
366-
parser.evaluate("a{} = 0".format(i))
367-
except Exception as e:
368-
print(len(parser.vars()), ArithmeticParser.MAX_VARS)
369-
print("{}: {}".format(type(e).__name__, e))
324+
parser.runTests("""\
325+
# DiceRollParser
326+
d20
327+
3d6
328+
d20 + 3d4
329+
(3d6)/3
330+
""", postParse=post_parse_evaluate)

test/test_unit.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,17 @@ def test_maximum_formula_depth(self, basic_arithmetic_parser):
222222
with pytest.raises(OverflowError):
223223
basic_arithmetic_parser.parse("e @= f")
224224

225+
def test_max_number_of_vars(self, basic_arithmetic_parser):
226+
VAR_LIMIT = 20
227+
basic_arithmetic_parser.max_number_of_vars = VAR_LIMIT
228+
229+
# compute number of vars that are safe to define by subtracting
230+
# the number of predefined vars from the allowed limit
231+
vars_to_define = VAR_LIMIT - len(basic_arithmetic_parser.vars())
232+
for i in range(vars_to_define):
233+
basic_arithmetic_parser.evaluate("a{} = 0".format(i))
234+
235+
# now define one more, which should put us over the limit and raise
236+
# the exception
237+
with pytest.raises(Exception):
238+
basic_arithmetic_parser.evaluate("a{} = 0".format(VAR_LIMIT))

0 commit comments

Comments
 (0)