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', 'JUMP_ABSOLUTE',
58 'DELETE_NAME', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE','MAKE_FUNCTION','JUMP_FORWARD',
59 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3'
62 _logger = logging.getLogger('safe_eval')
64 def _get_opcodes(codeobj):
65 """_get_opcodes(codeobj) -> [opcodes]
67 Extract the actual opcodes as a list from a code object
69 >>> c = compile("[1 + 2, (1,2)]", "", "eval")
71 [100, 100, 23, 100, 100, 102, 103, 83]
75 byte_codes = codeobj.co_code
76 while i < len(byte_codes):
77 code = ord(byte_codes[i])
79 if code >= HAVE_ARGUMENT:
85 def test_expr(expr, allowed_codes, mode="eval"):
86 """test_expr(expression, allowed_codes[, mode]) -> code_object
88 Test that the expression contains only the allowed opcodes.
89 If the expression is valid and contains only allowed codes,
90 return the compiled code object.
91 Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
95 # eval() does not like leading/trailing whitespace
97 code_obj = compile(expr, "", mode)
98 except (SyntaxError, TypeError):
99 _logger.debug('Invalid eval expression', exc_info=True)
102 _logger.debug('Disallowed or invalid eval expression', exc_info=True)
103 raise ValueError("%s is not a valid expression" % expr)
104 for code in _get_opcodes(code_obj):
105 if code not in allowed_codes:
106 raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
110 def const_eval(expr):
111 """const_eval(expression) -> value
113 Safe Python constant evaluation
115 Evaluates a string that contains an expression describing
116 a Python constant. Strings that are not valid Python expressions
117 or that contain other code besides the constant raise ValueError.
121 >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
122 [1, 2, (3, 4), {'foo': 'bar'}]
123 >>> const_eval("1+2")
124 Traceback (most recent call last):
126 ValueError: opcode BINARY_ADD not allowed
128 c = test_expr(expr, _CONST_OPCODES)
132 """expr_eval(expression) -> value
134 Restricted Python expression evaluation
136 Evaluates a string that contains an expression that only
137 uses Python constants. This can be used to e.g. evaluate
138 a numerical expression from an untrusted source.
142 >>> expr_eval("[1,2]*2")
144 >>> expr_eval("__import__('sys').modules")
145 Traceback (most recent call last):
147 ValueError: opcode LOAD_NAME not allowed
149 c = test_expr(expr, _EXPR_OPCODES)
153 # Port of Python 2.6's ast.literal_eval for use under Python 2.5
154 SAFE_CONSTANTS = {'None': None, 'True': True, 'False': False}
157 # first, try importing directly
158 from ast import literal_eval
163 if isinstance(node, ast.Str):
165 elif isinstance(node, ast.Num):
167 elif isinstance(node, ast.Tuple):
168 return tuple(map(_convert, node.elts))
169 elif isinstance(node, ast.List):
170 return list(map(_convert, node.elts))
171 elif isinstance(node, ast.Dict):
172 return dict((_convert(k), _convert(v)) for k, v
173 in zip(node.keys, node.values))
174 elif isinstance(node, ast.Name):
175 if node.id in SAFE_CONSTANTS:
176 return SAFE_CONSTANTS[node.id]
177 raise ValueError('malformed or disallowed expression')
179 def parse(expr, filename='<unknown>', mode='eval'):
180 """parse(source[, filename], mode]] -> code object
181 Parse an expression into an AST node.
182 Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
184 return compile(expr, filename, mode, ast.PyCF_ONLY_AST)
186 def literal_eval(node_or_string):
187 """literal_eval(expression) -> value
188 Safely evaluate an expression node or a string containing a Python
189 expression. The string or node provided may only consist of the
190 following Python literal structures: strings, numbers, tuples,
191 lists, dicts, booleans, and None.
193 >>> literal_eval('[1,True,"spam"]')
196 >>> literal_eval('1+3')
197 Traceback (most recent call last):
199 ValueError: malformed or disallowed expression
201 if isinstance(node_or_string, basestring):
202 node_or_string = parse(node_or_string)
203 if isinstance(node_or_string, ast.Expression):
204 node_or_string = node_or_string.body
205 return _convert(node_or_string)
209 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
210 """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
212 System-restricted Python expression evaluation
214 Evaluates a string that contains an expression that mostly
215 uses Python constants, arithmetic expressions and the
216 objects directly provided in context.
218 This can be used to e.g. evaluate
219 an OpenERP domain expression from an untrusted source.
221 Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
223 >>> safe_eval("__import__('sys').modules")
224 Traceback (most recent call last):
226 ValueError: opcode LOAD_NAME not allowed
229 if isinstance(expr, CodeType):
230 raise ValueError("safe_eval does not allow direct evaluation of code objects.")
232 if '__subclasses__' in expr:
233 raise ValueError('expression not allowed (__subclasses__)')
235 if globals_dict is None:
238 # prevent altering the globals/locals from within the sandbox
241 # isinstance() does not work below, we want *exactly* the dict class
242 if (globals_dict is not None and type(globals_dict) is not dict) \
243 or (locals_dict is not None and type(locals_dict) is not dict):
244 logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
245 "you should probably pass nocopy=True to safe_eval()')
247 globals_dict = dict(globals_dict)
248 if locals_dict is not None:
249 locals_dict = dict(locals_dict)
274 return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
276 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: