Skip to content

Commit 81f9d97

Browse files
committed
feat: Add a new eval helper function
1 parent f7e0a3a commit 81f9d97

2 files changed

Lines changed: 154 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: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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, True, False,
14+
0, 42, -17, 0xFF, 0o77, 0b1010,
15+
3.14, 1e10,
16+
1j,
17+
[], [1, "two", True, None],
18+
(), (1,), (1, 2, 3),
19+
{}, {"a": 1, "b": 2}, {1, 2, 3},
20+
[[1, 2], [3, 4]],
21+
{"a": {"b": {"c": [1, 2]}}},
22+
],
23+
)
24+
def test_literal(value):
25+
assert saferEval(str(value)) == value
26+
27+
@pytest.mark.parametrize(
28+
"input_str,expected",
29+
[
30+
('"hello"', "hello"),
31+
("'hello'", "hello"),
32+
("'a\\nb'", "a\nb"),
33+
(r"r'\n'", r"\n"),
34+
('"hello 🌍"', "hello 🌍"),
35+
("b'bytes'", b"bytes"),
36+
("b'\\xff'", b"\xff"),
37+
],
38+
)
39+
def test_string_literal(input_str, expected):
40+
assert saferEval(input_str) == expected
41+
42+
@pytest.mark.parametrize(
43+
"input_str",
44+
[
45+
"__import__('os').system('id')",
46+
"open('/etc/passwd').read()",
47+
"list()",
48+
"foo",
49+
"datetime.datetime.now()",
50+
"lambda x: x",
51+
"{k: v for k, v in []}",
52+
"(x for x in [])",
53+
"x == y",
54+
"1 + 2",
55+
"().__class__",
56+
"x[0]",
57+
"[1,2][1:]",
58+
"*1",
59+
"builtins.open",
60+
"object()",
61+
"MyList()",
62+
"f'{1+2}'",
63+
"@decorator",
64+
"assert True",
65+
"return 42",
66+
"x += 1",
67+
"with open('x') as f: pass",
68+
"for x in []: pass",
69+
"try: pass\nexcept: pass",
70+
"import os",
71+
"from os import path",
72+
"del x",
73+
"raise ValueError('x')",
74+
"yield 1",
75+
"await something",
76+
"(x := 1)",
77+
"(lambda x, /: x)(1)",
78+
"10**200",
79+
],
80+
)
81+
def test_rejected_inputs(input_str):
82+
with pytest.raises(ValueError):
83+
saferEval(input_str)
84+
85+
def test_max_len_exceeded():
86+
with pytest.raises(ValueError):
87+
saferEval("1" * 2049, 2048)
88+
89+
def test_max_len_custom_exceeded():
90+
with pytest.raises(ValueError):
91+
saferEval("[1, 2, 3]", 5)
92+
93+
def test_max_len_custom_ok():
94+
assert saferEval("[1, 2, 3]", 10) == [1, 2, 3]
95+
96+
def test_max_len_boundary_default():
97+
assert saferEval("42") == 42
98+
99+
@pytest.mark.parametrize("depth", [2000, 500])
100+
def test_deep_nesting(depth):
101+
with pytest.raises((ValueError, RecursionError)):
102+
saferEval("[" * depth + "1" + "]" * depth)
103+
104+
def test_large_string_literal():
105+
with pytest.raises(ValueError):
106+
saferEval("'" + "a" * 3000 + "'", 2048)
107+
108+
def test_large_list():
109+
with pytest.raises(ValueError):
110+
saferEval(str([1] * 3000), 2048)
111+
112+
@pytest.mark.parametrize(
113+
"s",
114+
[
115+
"{" + ", ".join(f'"k{i}": {i}' for i in range(50)) + "}",
116+
"[" + ",".join(str(i) for i in range(50)) + "]",
117+
],
118+
)
119+
def test_performance(s):
120+
start = time.time()
121+
saferEval(s, 2048)
122+
assert time.time() - start < 0.1

0 commit comments

Comments
 (0)