Skip to content

Commit

Permalink
Convert MAX_VARS and MAX_VAR_MEMORY from class constants to parser at…
Browse files Browse the repository at this point in the history
…tributes, 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;
  • Loading branch information
ptmcg committed May 24, 2020
1 parent 6f8e6fd commit 8186abf
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 58 deletions.
6 changes: 6 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ plusminus Change Log
A customizable parser attribute maximum_formula_depth will limit the number
of formula indirections. The default value is 12.

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


0.2.0 -

Expand Down
11 changes: 6 additions & 5 deletions plusminus/plusminus.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,6 @@ class ArithmeticParser:

LEFT = pp.opAssoc.LEFT
RIGHT = pp.opAssoc.RIGHT
MAX_VARS = 1000
MAX_VAR_MEMORY = 10 ** 6

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

def __init__(self):
self.max_number_of_vars = 1000
self.max_var_memory = 10 ** 6

self._added_operator_specs = []
self._added_function_specs = {}
self._base_operators = (
Expand Down Expand Up @@ -951,7 +952,7 @@ def eval_and_store_value(tokens):
var_name = lhs_name.name
if (
var_name not in assigned_vars
and len(assigned_vars) >= self.MAX_VARS
and len(assigned_vars) >= self.max_number_of_vars
):
raise Exception("too many variables defined")
assigned_vars[var_name] = rval
Expand Down Expand Up @@ -1024,8 +1025,8 @@ def get_depth(formula_node):
assigned_vars = identifier_node_class._assigned_vars
if (
dest_var_name not in assigned_vars
and len(assigned_vars) >= self.MAX_VARS
or sum(sys.getsizeof(vv) for vv in assigned_vars.values()) > self.MAX_VAR_MEMORY
and len(assigned_vars) >= self.max_number_of_vars
or sum(sys.getsizeof(vv) for vv in assigned_vars.values()) > self.max_var_memory
):
raise Exception("too many variables defined")
assigned_vars[dest_var_name] = rval
Expand Down
67 changes: 14 additions & 53 deletions test/arith_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,9 @@ def post_parse_evaluate(teststr, result):
print('circle_area =', parser['circle_area'])
print('circle_area =', parser.evaluate('circle_area'))

print("del parser['circle_radius']")
del parser['circle_radius']

try:
print('circle_area =', end=' ')
print(parser.evaluate('circle_area'))
Expand All @@ -254,9 +256,9 @@ def post_parse_evaluate(teststr, result):
print(parser.parse("6.02e24 * 100").evaluate())



parser = CombinatoricsArithmeticParser()
parser.runTests("""\
# CombinatoricsArithmeticParser
3!
-3!
3!!
Expand All @@ -272,6 +274,7 @@ def post_parse_evaluate(teststr, result):

parser = BusinessArithmeticParser()
parser.runTests("""\
# BusinessArithmeticParser
25%
20 * 50%
50% of 20
Expand All @@ -284,6 +287,7 @@ def post_parse_evaluate(teststr, result):
""",
postParse=post_parse_evaluate)


from datetime import datetime
class DateTimeArithmeticParser(ArithmeticParser):
SECONDS_PER_MINUTE = 60
Expand All @@ -302,8 +306,10 @@ def customize(self):
microsecond=0).timestamp())
self.add_function('str', 1, lambda dt: str(datetime.fromtimestamp(dt)))


parser = DateTimeArithmeticParser()
parser.runTests("""\
# DateTimeArithmeticParser
now()
str(now())
str(today())
Expand All @@ -315,55 +321,10 @@ def customize(self):


parser = DiceRollParser()
parser.runTests("""
d20
3d6
d20 + 3d4
(3d6)/3
""", postParse=post_parse_evaluate)


print()


# override max number of variables
class restore:
"""
Context manager for restoring an object's attributes back the way they were if they were
changed or deleted, or to remove any attributes that were added.
"""
def __init__(self, obj, *attr_names):
self._obj = obj
self._attrs = attr_names
if not self._attrs:
self._attrs = [name for name in vars(obj) if name not in ('__dict__', '__slots__')]
self._no_attr_value = object()
self._save_values = {}

def __enter__(self):
for attr in self._attrs:
self._save_values[attr] = getattr(self._obj, attr, self._no_attr_value)
return self

def __exit__(self, *args):
for attr in self._attrs:
save_value = self._save_values[attr]
if save_value is not self._no_attr_value:
if getattr(self._obj, attr, self._no_attr_value) != save_value:
print("reset", attr, "to", save_value)
setattr(self._obj, attr, save_value)
else:
if hasattr(self._obj, attr):
delattr(self._obj, attr)


print('test defining too many vars (set max to 20)')
with restore(ArithmeticParser):
ArithmeticParser.MAX_VARS = 20
parser = ArithmeticParser()
try:
for i in range(1000):
parser.evaluate("a{} = 0".format(i))
except Exception as e:
print(len(parser.vars()), ArithmeticParser.MAX_VARS)
print("{}: {}".format(type(e).__name__, e))
parser.runTests("""\
# DiceRollParser
d20
3d6
d20 + 3d4
(3d6)/3
""", postParse=post_parse_evaluate)
14 changes: 14 additions & 0 deletions test/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,17 @@ def test_maximum_formula_depth(self, basic_arithmetic_parser):
with pytest.raises(OverflowError):
basic_arithmetic_parser.parse("e @= f")

def test_max_number_of_vars(self, basic_arithmetic_parser):
VAR_LIMIT = 20
basic_arithmetic_parser.max_number_of_vars = VAR_LIMIT

# compute number of vars that are safe to define by subtracting
# the number of predefined vars from the allowed limit
vars_to_define = VAR_LIMIT - len(basic_arithmetic_parser.vars())
for i in range(vars_to_define):
basic_arithmetic_parser.evaluate("a{} = 0".format(i))

# now define one more, which should put us over the limit and raise
# the exception
with pytest.raises(Exception):
basic_arithmetic_parser.evaluate("a{} = 0".format(VAR_LIMIT))

0 comments on commit 8186abf

Please sign in to comment.