forked from ideasman42/py_safe_math_eval
-
Notifications
You must be signed in to change notification settings - Fork 0
/
safe_eval.py
170 lines (135 loc) · 3.22 KB
/
safe_eval.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# Apache 2.0 licensed
"""
This module provides functions for evaluating Python code,
which is intended to be secure.
Warning: Since this module is new, its possible there are flaws in it.
How it Works
============
- Restrict built-in namespace access.
- Restrict byte-codes used.
What Works
==========
This works:
a ** cos(b / c) - sin(d)
This will fail:
a.b
__import__("os").unlink("path")
"""
__all__ = (
"safe_eval",
"raise_if_code_unsafe",
)
import dis
builtins_whitelist = {
# basic funcs
"all",
"any",
"len",
"max",
"min",
# types
"bool",
"float",
"int",
# math
"abs",
"divmod",
"pow",
"round",
"sum",
}
opcode_whitelist = {
'NOP',
'UNARY_POSITIVE',
'UNARY_NEGATIVE',
'UNARY_NOT',
'UNARY_INVERT',
'BINARY_MATRIX_MULTIPLY',
'INPLACE_MATRIX_MULTIPLY',
'BINARY_POWER',
'BINARY_MULTIPLY',
'BINARY_MODULO',
'BINARY_ADD',
'BINARY_SUBTRACT',
'BINARY_SUBSCR',
'BINARY_FLOOR_DIVIDE',
'BINARY_TRUE_DIVIDE',
'INPLACE_FLOOR_DIVIDE',
'INPLACE_TRUE_DIVIDE',
'INPLACE_ADD',
'INPLACE_SUBTRACT',
'INPLACE_MULTIPLY',
'INPLACE_MODULO',
'BINARY_LSHIFT',
'BINARY_RSHIFT',
'BINARY_AND',
'BINARY_XOR',
'BINARY_OR',
'INPLACE_POWER',
'INPLACE_LSHIFT',
'INPLACE_RSHIFT',
'INPLACE_AND',
'INPLACE_XOR',
'INPLACE_OR',
'RETURN_VALUE',
'LOAD_CONST',
'LOAD_NAME',
'BUILD_TUPLE',
'BUILD_LIST',
'BUILD_SET',
'BUILD_MAP',
'COMPARE_OP',
'JUMP_FORWARD',
'JUMP_IF_FALSE_OR_POP',
'JUMP_IF_TRUE_OR_POP',
'JUMP_ABSOLUTE',
'POP_JUMP_IF_FALSE',
'POP_JUMP_IF_TRUE',
'LOAD_GLOBAL',
'LOAD_FAST',
'STORE_FAST',
'DELETE_FAST',
'CALL_FUNCTION',
'LOAD_DEREF',
'STORE_DEREF',
'CALL_FUNCTION_VAR',
'CALL_FUNCTION_KW',
'CALL_FUNCTION_VAR_KW',
}
# Convert names to index
opname_reverse = {name: index for index, name in enumerate(dis.opname)}
opcode_whitelist_index = {opname_reverse[name] for name in opcode_whitelist}
def raise_if_code_unsafe(code, globals=None, locals=None):
whitelist = set(builtins_whitelist)
if globals:
whitelist.update(globals)
if locals:
whitelist.update(locals)
bad_ops = []
for name in code.co_names:
if name not in whitelist:
bad_ops.append(name)
if bad_ops:
raise RuntimeError(
"Name(s) %s not in white-list: (%s)" % (
", ".join(repr(name) for name in bad_ops),
", ".join(sorted(whitelist)))
)
del bad_ops
code_bytes = code.co_code
def code_size(opcode):
if opcode >= dis.HAVE_ARGUMENT:
return 3
else:
return 1
i = 0
code_len = len(code_bytes)
while i < code_len:
opcode = code_bytes[i]
if opcode not in opcode_whitelist_index:
raise RuntimeError("OpCode %r not in white-list" % dis.opname[opcode])
i += code_size(opcode)
def safe_eval(source, globals=None, locals=None):
code = compile(source, "<safe_eval>", "eval")
raise_if_code_unsafe(code, globals=globals, locals=locals)
return eval(code, globals, locals)