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 # The time module is usually already provided in the safe_eval environment
43 # but some code, e.g. datetime.datetime.now() (Windows/Python 2.5.2, bug
44 # lp:703841), does import time.
45 _ALLOWED_MODULES = ['_strptime', 'time']
47 _CONST_OPCODES = set(opmap[x] for x in [
48 'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP','POP_BLOCK','SETUP_LOOP',
49 'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
50 'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
52 _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
53 'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
54 'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY',
55 'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE',
56 'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR',
57 'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR',
58 'BINARY_OR'] if x in opmap))
60 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
61 'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
62 'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
63 'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
64 'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
65 # New in Python 2.7 - http://bugs.python.org/issue4715 :
66 'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
70 _logger = logging.getLogger('safe_eval')
72 def _get_opcodes(codeobj):
73 """_get_opcodes(codeobj) -> [opcodes]
75 Extract the actual opcodes as a list from a code object
77 >>> c = compile("[1 + 2, (1,2)]", "", "eval")
79 [100, 100, 23, 100, 100, 102, 103, 83]
83 byte_codes = codeobj.co_code
84 while i < len(byte_codes):
85 code = ord(byte_codes[i])
87 if code >= HAVE_ARGUMENT:
93 def test_expr(expr, allowed_codes, mode="eval"):
94 """test_expr(expression, allowed_codes[, mode]) -> code_object
96 Test that the expression contains only the allowed opcodes.
97 If the expression is valid and contains only allowed codes,
98 return the compiled code object.
99 Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
103 # eval() does not like leading/trailing whitespace
105 code_obj = compile(expr, "", mode)
106 except (SyntaxError, TypeError):
107 _logger.debug('Invalid eval expression', exc_info=True)
110 _logger.debug('Disallowed or invalid eval expression', exc_info=True)
111 raise ValueError("%s is not a valid expression" % expr)
112 for code in _get_opcodes(code_obj):
113 if code not in allowed_codes:
114 raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
118 def const_eval(expr):
119 """const_eval(expression) -> value
121 Safe Python constant evaluation
123 Evaluates a string that contains an expression describing
124 a Python constant. Strings that are not valid Python expressions
125 or that contain other code besides the constant raise ValueError.
129 >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
130 [1, 2, (3, 4), {'foo': 'bar'}]
131 >>> const_eval("1+2")
132 Traceback (most recent call last):
134 ValueError: opcode BINARY_ADD not allowed
136 c = test_expr(expr, _CONST_OPCODES)
140 """expr_eval(expression) -> value
142 Restricted Python expression evaluation
144 Evaluates a string that contains an expression that only
145 uses Python constants. This can be used to e.g. evaluate
146 a numerical expression from an untrusted source.
150 >>> expr_eval("[1,2]*2")
152 >>> expr_eval("__import__('sys').modules")
153 Traceback (most recent call last):
155 ValueError: opcode LOAD_NAME not allowed
157 c = test_expr(expr, _EXPR_OPCODES)
161 # Port of Python 2.6's ast.literal_eval for use under Python 2.5
162 SAFE_CONSTANTS = {'None': None, 'True': True, 'False': False}
165 # first, try importing directly
166 from ast import literal_eval
171 if isinstance(node, ast.Str):
173 elif isinstance(node, ast.Num):
175 elif isinstance(node, ast.Tuple):
176 return tuple(map(_convert, node.elts))
177 elif isinstance(node, ast.List):
178 return list(map(_convert, node.elts))
179 elif isinstance(node, ast.Dict):
180 return dict((_convert(k), _convert(v)) for k, v
181 in zip(node.keys, node.values))
182 elif isinstance(node, ast.Name):
183 if node.id in SAFE_CONSTANTS:
184 return SAFE_CONSTANTS[node.id]
185 raise ValueError('malformed or disallowed expression')
187 def parse(expr, filename='<unknown>', mode='eval'):
188 """parse(source[, filename], mode]] -> code object
189 Parse an expression into an AST node.
190 Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
192 return compile(expr, filename, mode, ast.PyCF_ONLY_AST)
194 def literal_eval(node_or_string):
195 """literal_eval(expression) -> value
196 Safely evaluate an expression node or a string containing a Python
197 expression. The string or node provided may only consist of the
198 following Python literal structures: strings, numbers, tuples,
199 lists, dicts, booleans, and None.
201 >>> literal_eval('[1,True,"spam"]')
204 >>> literal_eval('1+3')
205 Traceback (most recent call last):
207 ValueError: malformed or disallowed expression
209 if isinstance(node_or_string, basestring):
210 node_or_string = parse(node_or_string)
211 if isinstance(node_or_string, ast.Expression):
212 node_or_string = node_or_string.body
213 return _convert(node_or_string)
215 def _import(name, globals={}, locals={}, fromlist=[], level=-1):
216 if name in _ALLOWED_MODULES:
217 return __import__(name, globals, locals, level)
218 raise ImportError(name)
220 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
221 """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
223 System-restricted Python expression evaluation
225 Evaluates a string that contains an expression that mostly
226 uses Python constants, arithmetic expressions and the
227 objects directly provided in context.
229 This can be used to e.g. evaluate
230 an OpenERP domain expression from an untrusted source.
232 Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
234 >>> safe_eval("__import__('sys').modules")
235 Traceback (most recent call last):
237 ValueError: opcode LOAD_NAME not allowed
240 if isinstance(expr, CodeType):
241 raise ValueError("safe_eval does not allow direct evaluation of code objects.")
243 if '__subclasses__' in expr:
244 raise ValueError('expression not allowed (__subclasses__)')
246 if globals_dict is None:
249 # prevent altering the globals/locals from within the sandbox
252 # isinstance() does not work below, we want *exactly* the dict class
253 if (globals_dict is not None and type(globals_dict) is not dict) \
254 or (locals_dict is not None and type(locals_dict) is not dict):
255 logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
256 "you should probably pass nocopy=True to safe_eval()')
258 globals_dict = dict(globals_dict)
259 if locals_dict is not None:
260 locals_dict = dict(locals_dict)
264 '__import__': _import,
284 return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
286 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: