Skip to content

Commit b60a883

Browse files
committed
An alternative approach to avoiding arbitrary evaluation of code when parsing strings as units.
Based on https://stackoverflow.com/a/11952618 by https://stackoverflow.com/users/567292/ecatmur Fixes python-quantities#250 Also added test for the original issue: python-quantities#221
1 parent 93f44a3 commit b60a883

File tree

3 files changed

+37
-20
lines changed

3 files changed

+37
-20
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ jobs:
118118
python -m pip install -U setuptools
119119
python -m pip install -U wheel
120120
python -m pip install "numpy==${{ matrix.numpy-version }}"
121+
python -m pip install -U pytest
121122
python -m pip install -U mypy
122123
123124
- name: Install

quantities/registry.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,49 @@
11
"""
22
"""
33

4+
import ast
45
import re
5-
import builtins
66

77

88
class UnitRegistry:
9+
# Note that this structure ensures that UnitRegistry behaves as a singleton
910

1011
class __Registry:
1112

1213
__shared_state = {}
14+
whitelist = (
15+
ast.Expression,
16+
ast.Constant,
17+
ast.Name,
18+
ast.Load,
19+
ast.BinOp,
20+
ast.UnaryOp,
21+
ast.operator,
22+
ast.unaryop,
23+
ast.Num,
24+
)
1325

1426
def __init__(self):
1527
self.__dict__ = self.__shared_state
1628
self.__context = {}
1729

1830
def __getitem__(self, string):
19-
20-
# easy hack to prevent arbitrary evaluation of code
21-
all_builtins = dir(builtins)
22-
# because we have kilobytes, other bytes we have to remove bytes
23-
all_builtins.remove("bytes")
24-
# have to deal with octet as well
25-
all_builtins.remove("oct")
26-
# have to remove min which is short for minute
27-
all_builtins.remove("min")
28-
for builtin in all_builtins:
29-
if builtin in string:
30-
raise RuntimeError(f"String parsing error for `{string}`. Enter a string accepted by quantities")
31-
32-
try:
33-
return eval(string, self.__context)
34-
except NameError:
31+
tree = ast.parse(string, mode="eval")
32+
valid = all(isinstance(node, self.whitelist) for node in ast.walk(tree))
33+
if valid:
34+
try:
35+
item = eval(
36+
compile(tree, filename="", mode="eval"),
37+
{"__builtins__": {}},
38+
self.__context,
39+
)
40+
except NameError:
41+
raise LookupError('Unable to parse units: "%s"' % string)
42+
else:
43+
return item
44+
else:
3545
# could return self['UnitQuantity'](string)
36-
raise LookupError(
37-
'Unable to parse units: "%s"'%string
38-
)
46+
raise LookupError('Unable to parse units: "%s"' % string)
3947

4048
def __setitem__(self, string, val):
4149
assert isinstance(string, str)

quantities/tests/test_units.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import pytest
2+
13
from .. import units as pq
24
from .common import TestCase
35

6+
47
class TestUnits(TestCase):
58

69
def test_compound_units(self):
@@ -30,3 +33,8 @@ def test_units_copy(self):
3033
self.assertQuantityEqual(pq.m.copy(), pq.m)
3134
pc_per_cc = pq.CompoundUnit("pc/cm**3")
3235
self.assertQuantityEqual(pc_per_cc.copy(), pc_per_cc)
36+
37+
def test_code_injection(self):
38+
with pytest.raises(LookupError) as exc_info:
39+
pq.CompoundUnit("exec(\"print('Hello there.')\\nprint('General Wasabi!')\")")
40+
assert "Wasabi" in str(exc_info.value)

0 commit comments

Comments
 (0)