1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 # Copyright (C) 2004-2010 OpenERP s.a. (<http://www.openerp.com>).
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 ##############################################################################
21 safe_eval module - methods intended to provide more restricted alternatives to
22 evaluate simple and/or untrusted code.
24 Methods in this module are typically used as alternatives to eval() to parse
25 OpenERP domain strings, conditions and expressions, mostly based on locals
26 condition/math builtins.
29 # Module partially ripped from/inspired by several different sources:
30 # - http://code.activestate.com/recipes/286134/
31 # - safe_eval in lp:~xrg/openobject-server/optimize-5.0
32 # - safe_eval in tryton http://hg.tryton.org/hgwebdir.cgi/trytond/rev/bbb5f73319ad
33 # - python 2.6's ast.literal_eval
35 from opcode import HAVE_ARGUMENT, opmap, opname
36 from types import CodeType
40 __all__ = ['test_expr', 'literal_eval', 'safe_eval', 'const_eval', 'ext_eval' ]
42 _CONST_OPCODES = set(opmap[x] for x in [
43 'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP','POP_BLOCK','SETUP_LOOP',
44 'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
45 'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
47 _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
48 'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
49 'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY',
50 'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE',
51 'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR',
52 'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR',
53 'BINARY_OR'] if x in opmap))
55 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
56 'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
57 'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
58 'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
59 'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
60 # New in Python 2.7 - http://bugs.python.org/issue4715 :
61 'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
65 _logger = logging.getLogger('safe_eval')
67 def _get_opcodes(codeobj):
68 """_get_opcodes(codeobj) -> [opcodes]
70 Extract the actual opcodes as a list from a code object
72 >>> c = compile("[1 + 2, (1,2)]", "", "eval")
74 [100, 100, 23, 100, 100, 102, 103, 83]
78 byte_codes = codeobj.co_code
79 while i < len(byte_codes):
80 code = ord(byte_codes[i])
82 if code >= HAVE_ARGUMENT:
88 def test_expr(expr, allowed_codes, mode="eval"):
89 """test_expr(expression, allowed_codes[, mode]) -> code_object
91 Test that the expression contains only the allowed opcodes.
92 If the expression is valid and contains only allowed codes,
93 return the compiled code object.
94 Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
98 # eval() does not like leading/trailing whitespace
100 code_obj = compile(expr, "", mode)
101 except (SyntaxError, TypeError):
102 _logger.debug('Invalid eval expression', exc_info=True)
105 _logger.debug('Disallowed or invalid eval expression', exc_info=True)
106 raise ValueError("%s is not a valid expression" % expr)
107 for code in _get_opcodes(code_obj):
108 if code not in allowed_codes:
109 raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
113 def const_eval(expr):
114 """const_eval(expression) -> value
116 Safe Python constant evaluation
118 Evaluates a string that contains an expression describing
119 a Python constant. Strings that are not valid Python expressions
120 or that contain other code besides the constant raise ValueError.
124 >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
125 [1, 2, (3, 4), {'foo': 'bar'}]
126 >>> const_eval("1+2")
127 Traceback (most recent call last):
129 ValueError: opcode BINARY_ADD not allowed
131 c = test_expr(expr, _CONST_OPCODES)
135 """expr_eval(expression) -> value
137 Restricted Python expression evaluation
139 Evaluates a string that contains an expression that only
140 uses Python constants. This can be used to e.g. evaluate
141 a numerical expression from an untrusted source.
145 >>> expr_eval("[1,2]*2")
147 >>> expr_eval("__import__('sys').modules")
148 Traceback (most recent call last):
150 ValueError: opcode LOAD_NAME not allowed
152 c = test_expr(expr, _EXPR_OPCODES)
156 # Port of Python 2.6's ast.literal_eval for use under Python 2.5
157 SAFE_CONSTANTS = {'None': None, 'True': True, 'False': False}
160 # first, try importing directly
161 from ast import literal_eval
166 if isinstance(node, ast.Str):
168 elif isinstance(node, ast.Num):
170 elif isinstance(node, ast.Tuple):
171 return tuple(map(_convert, node.elts))
172 elif isinstance(node, ast.List):
173 return list(map(_convert, node.elts))
174 elif isinstance(node, ast.Dict):
175 return dict((_convert(k), _convert(v)) for k, v
176 in zip(node.keys, node.values))
177 elif isinstance(node, ast.Name):
178 if node.id in SAFE_CONSTANTS:
179 return SAFE_CONSTANTS[node.id]
180 raise ValueError('malformed or disallowed expression')
182 def parse(expr, filename='<unknown>', mode='eval'):
183 """parse(source[, filename], mode]] -> code object
184 Parse an expression into an AST node.
185 Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
187 return compile(expr, filename, mode, ast.PyCF_ONLY_AST)
189 def literal_eval(node_or_string):
190 """literal_eval(expression) -> value
191 Safely evaluate an expression node or a string containing a Python
192 expression. The string or node provided may only consist of the
193 following Python literal structures: strings, numbers, tuples,
194 lists, dicts, booleans, and None.
196 >>> literal_eval('[1,True,"spam"]')
199 >>> literal_eval('1+3')
200 Traceback (most recent call last):
202 ValueError: malformed or disallowed expression
204 if isinstance(node_or_string, basestring):
205 node_or_string = parse(node_or_string)
206 if isinstance(node_or_string, ast.Expression):
207 node_or_string = node_or_string.body
208 return _convert(node_or_string)
212 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
213 """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
215 System-restricted Python expression evaluation
217 Evaluates a string that contains an expression that mostly
218 uses Python constants, arithmetic expressions and the
219 objects directly provided in context.
221 This can be used to e.g. evaluate
222 an OpenERP domain expression from an untrusted source.
224 Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
226 >>> safe_eval("__import__('sys').modules")
227 Traceback (most recent call last):
229 ValueError: opcode LOAD_NAME not allowed
232 if isinstance(expr, CodeType):
233 raise ValueError("safe_eval does not allow direct evaluation of code objects.")
235 if '__subclasses__' in expr:
236 raise ValueError('expression not allowed (__subclasses__)')
238 if globals_dict is None:
241 # prevent altering the globals/locals from within the sandbox
244 # isinstance() does not work below, we want *exactly* the dict class
245 if (globals_dict is not None and type(globals_dict) is not dict) \
246 or (locals_dict is not None and type(locals_dict) is not dict):
247 logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
248 "you should probably pass nocopy=True to safe_eval()')
250 globals_dict = dict(globals_dict)
251 if locals_dict is not None:
252 locals_dict = dict(locals_dict)
275 return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
277 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: