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
34 from opcode import HAVE_ARGUMENT, opmap, opname
35 from types import CodeType
39 __all__ = ['test_expr', 'safe_eval', 'const_eval', 'ext_eval' ]
41 # The time module is usually already provided in the safe_eval environment
42 # but some code, e.g. datetime.datetime.now() (Windows/Python 2.5.2, bug
43 # lp:703841), does import time.
44 _ALLOWED_MODULES = ['_strptime', 'time']
46 _CONST_OPCODES = set(opmap[x] for x in [
47 'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP', 'DUP_TOPX',
48 'POP_BLOCK','SETUP_LOOP', 'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
49 'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
51 _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
52 'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
53 'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY',
54 'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE',
55 'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR',
56 'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR',
57 'BINARY_OR', 'INPLACE_ADD', 'INPLACE_SUBTRACT', 'INPLACE_MULTIPLY',
58 'INPLACE_DIVIDE', 'INPLACE_REMAINDER', 'INPLACE_POWER',
59 'INPLACE_LEFTSHIFT', 'INPLACE_RIGHTSHIFT', 'INPLACE_AND',
60 'INPLACE_XOR','INPLACE_OR'
63 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
64 'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
65 'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
66 'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
67 'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
68 # New in Python 2.7 - http://bugs.python.org/issue4715 :
69 'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
70 'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY'
73 _logger = logging.getLogger('safe_eval')
75 def _get_opcodes(codeobj):
76 """_get_opcodes(codeobj) -> [opcodes]
78 Extract the actual opcodes as a list from a code object
80 >>> c = compile("[1 + 2, (1,2)]", "", "eval")
82 [100, 100, 23, 100, 100, 102, 103, 83]
86 byte_codes = codeobj.co_code
87 while i < len(byte_codes):
88 code = ord(byte_codes[i])
90 if code >= HAVE_ARGUMENT:
96 def test_expr(expr, allowed_codes, mode="eval"):
97 """test_expr(expression, allowed_codes[, mode]) -> code_object
99 Test that the expression contains only the allowed opcodes.
100 If the expression is valid and contains only allowed codes,
101 return the compiled code object.
102 Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
106 # eval() does not like leading/trailing whitespace
108 code_obj = compile(expr, "", mode)
109 except (SyntaxError, TypeError):
110 _logger.debug('Invalid eval expression', exc_info=True)
113 _logger.debug('Disallowed or invalid eval expression', exc_info=True)
114 raise ValueError("%s is not a valid expression" % expr)
115 for code in _get_opcodes(code_obj):
116 if code not in allowed_codes:
117 raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
121 def const_eval(expr):
122 """const_eval(expression) -> value
124 Safe Python constant evaluation
126 Evaluates a string that contains an expression describing
127 a Python constant. Strings that are not valid Python expressions
128 or that contain other code besides the constant raise ValueError.
132 >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
133 [1, 2, (3, 4), {'foo': 'bar'}]
134 >>> const_eval("1+2")
135 Traceback (most recent call last):
137 ValueError: opcode BINARY_ADD not allowed
139 c = test_expr(expr, _CONST_OPCODES)
143 """expr_eval(expression) -> value
145 Restricted Python expression evaluation
147 Evaluates a string that contains an expression that only
148 uses Python constants. This can be used to e.g. evaluate
149 a numerical expression from an untrusted source.
153 >>> expr_eval("[1,2]*2")
155 >>> expr_eval("__import__('sys').modules")
156 Traceback (most recent call last):
158 ValueError: opcode LOAD_NAME not allowed
160 c = test_expr(expr, _EXPR_OPCODES)
163 def _import(name, globals=None, locals=None, fromlist=None, level=-1):
170 if name in _ALLOWED_MODULES:
171 return __import__(name, globals, locals, level)
172 raise ImportError(name)
174 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
175 """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
177 System-restricted Python expression evaluation
179 Evaluates a string that contains an expression that mostly
180 uses Python constants, arithmetic expressions and the
181 objects directly provided in context.
183 This can be used to e.g. evaluate
184 an OpenERP domain expression from an untrusted source.
186 Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
188 >>> safe_eval("__import__('sys').modules")
189 Traceback (most recent call last):
191 ValueError: opcode LOAD_NAME not allowed
194 if isinstance(expr, CodeType):
195 raise ValueError("safe_eval does not allow direct evaluation of code objects.")
197 if '__subclasses__' in expr:
198 raise ValueError('expression not allowed (__subclasses__)')
200 if globals_dict is None:
203 # prevent altering the globals/locals from within the sandbox
206 # isinstance() does not work below, we want *exactly* the dict class
207 if (globals_dict is not None and type(globals_dict) is not dict) \
208 or (locals_dict is not None and type(locals_dict) is not dict):
209 logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
210 "you should probably pass nocopy=True to safe_eval()')
212 globals_dict = dict(globals_dict)
213 if locals_dict is not None:
214 locals_dict = dict(locals_dict)
218 '__import__': _import,
240 return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
242 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: