Skip to content

Commit 169edf5

Browse files
committed
feat: Add a new eval helper function
1 parent a975d69 commit 169edf5

2 files changed

Lines changed: 177 additions & 0 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env python3
2+
3+
import ast
4+
5+
6+
def saferEval(obj_str, max_len=2048):
7+
"""This function adds an extra length check around literal_eval.
8+
On python3.11 and above (which has a recursion guard), this should
9+
be safe enough for use on general authenticated user input.
10+
11+
Note: This doesn't handle all of the cases of eval, such as
12+
datetime as those are technically executing code to
13+
instantiate the non-base objects.
14+
"""
15+
# Ensure input is a string
16+
obj_str = str(obj_str)
17+
if len(obj_str) > max_len:
18+
raise ValueError(f"Object string is too long (>{max_len} bytes)")
19+
try:
20+
return ast.literal_eval(obj_str)
21+
except (ValueError, TypeError, SyntaxError):
22+
# This covers all of the cases where the string is wrong (unclosed brackets...)
23+
# or contains disallowed items like function calls or non-expression.
24+
raise ValueError("Syntax error processing object expression")
25+
except (MemoryError, RecursionError):
26+
# This is encountered if the object is nested too deeply and other structures
27+
# that are probably malicious.
28+
raise ValueError("Object expression too large")
29+
except Exception:
30+
# There are no other possible exceptions at the time of writing,
31+
# this is to catch any added in future python versions.
32+
raise ValueError("Unknown error processing object expression")
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Tests for saferEval – uses pytest parametrize for conciseness."""
2+
3+
import time
4+
5+
import pytest
6+
7+
from DIRAC.Core.Utilities.SaferEval import saferEval
8+
9+
10+
@pytest.mark.parametrize(
11+
"value",
12+
[
13+
None,
14+
True,
15+
False,
16+
0,
17+
42,
18+
-17,
19+
0xFF,
20+
0o77,
21+
0b1010,
22+
3.14,
23+
1e10,
24+
1j,
25+
[],
26+
[1, "two", True, None],
27+
(),
28+
(1,),
29+
(1, 2, 3),
30+
{},
31+
{"a": 1, "b": 2},
32+
{1, 2, 3},
33+
[[1, 2], [3, 4]],
34+
{"a": {"b": {"c": [1, 2]}}},
35+
],
36+
)
37+
def test_literal(value):
38+
assert saferEval(str(value)) == value
39+
40+
41+
@pytest.mark.parametrize(
42+
"input_str,expected",
43+
[
44+
('"hello"', "hello"),
45+
("'hello'", "hello"),
46+
("'a\\nb'", "a\nb"),
47+
(r"r'\n'", r"\n"),
48+
('"hello 🌍"', "hello 🌍"),
49+
("b'bytes'", b"bytes"),
50+
("b'\\xff'", b"\xff"),
51+
],
52+
)
53+
def test_string_literal(input_str, expected):
54+
assert saferEval(input_str) == expected
55+
56+
57+
@pytest.mark.parametrize(
58+
"input_str",
59+
[
60+
"__import__('os').system('id')",
61+
"open('/etc/passwd').read()",
62+
"list()",
63+
"foo",
64+
"datetime.datetime.now()",
65+
"lambda x: x",
66+
"{k: v for k, v in []}",
67+
"(x for x in [])",
68+
"x == y",
69+
"1 + 2",
70+
"().__class__",
71+
"x[0]",
72+
"[1,2][1:]",
73+
"*1",
74+
"builtins.open",
75+
"object()",
76+
"MyList()",
77+
"f'{1+2}'",
78+
"@decorator",
79+
"assert True",
80+
"return 42",
81+
"x += 1",
82+
"with open('x') as f: pass",
83+
"for x in []: pass",
84+
"try: pass\nexcept: pass",
85+
"import os",
86+
"from os import path",
87+
"del x",
88+
"raise ValueError('x')",
89+
"yield 1",
90+
"await something",
91+
"(x := 1)",
92+
"(lambda x, /: x)(1)",
93+
"10**200",
94+
],
95+
)
96+
def test_rejected_inputs(input_str):
97+
with pytest.raises(ValueError):
98+
saferEval(input_str)
99+
100+
101+
def test_max_len_exceeded():
102+
with pytest.raises(ValueError):
103+
saferEval("1" * 2049, 2048)
104+
105+
106+
def test_max_len_custom_exceeded():
107+
with pytest.raises(ValueError):
108+
saferEval("[1, 2, 3]", 5)
109+
110+
111+
def test_max_len_custom_ok():
112+
assert saferEval("[1, 2, 3]", 10) == [1, 2, 3]
113+
114+
115+
def test_max_len_boundary_default():
116+
assert saferEval("42") == 42
117+
118+
119+
@pytest.mark.parametrize("depth", [2000, 500])
120+
def test_deep_nesting(depth):
121+
with pytest.raises((ValueError, RecursionError)):
122+
saferEval("[" * depth + "1" + "]" * depth)
123+
124+
125+
def test_large_string_literal():
126+
with pytest.raises(ValueError):
127+
saferEval("'" + "a" * 3000 + "'", 2048)
128+
129+
130+
def test_large_list():
131+
with pytest.raises(ValueError):
132+
saferEval(str([1] * 3000), 2048)
133+
134+
135+
@pytest.mark.parametrize(
136+
"s",
137+
[
138+
"{" + ", ".join(f'"k{i}": {i}' for i in range(50)) + "}",
139+
"[" + ",".join(str(i) for i in range(50)) + "]",
140+
],
141+
)
142+
def test_performance(s):
143+
start = time.time()
144+
saferEval(s, 2048)
145+
assert time.time() - start < 0.1

0 commit comments

Comments
 (0)