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', 'DUP_TOPX',
49 'POP_BLOCK','SETUP_LOOP', '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', 'INPLACE_ADD', 'INPLACE_SUBTRACT', 'INPLACE_MULTIPLY',
59 'INPLACE_DIVIDE', 'INPLACE_REMAINDER', 'INPLACE_POWER',
60 'INPLACE_LEFTSHIFT', 'INPLACE_RIGHTSHIFT', 'INPLACE_AND',
61 'INPLACE_XOR','INPLACE_OR'
64 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
65 'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
66 'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
67 'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
68 'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
69 # New in Python 2.7 - http://bugs.python.org/issue4715 :
70 'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
71 'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY'
74 _logger = logging.getLogger('safe_eval')
76 def _get_opcodes(codeobj):
77 """_get_opcodes(codeobj) -> [opcodes]
79 Extract the actual opcodes as a list from a code object
81 >>> c = compile("[1 + 2, (1,2)]", "", "eval")
83 [100, 100, 23, 100, 100, 102, 103, 83]
87 byte_codes = codeobj.co_code
88 while i < len(byte_codes):
89 code = ord(byte_codes[i])
91 if code >= HAVE_ARGUMENT:
97 def test_expr(expr, allowed_codes, mode="eval"):
98 """test_expr(expression, allowed_codes[, mode]) -> code_object
100 Test that the expression contains only the allowed opcodes.
101 If the expression is valid and contains only allowed codes,
102 return the compiled code object.
103 Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
107 # eval() does not like leading/trailing whitespace
109 code_obj = compile(expr, "", mode)
110 except (SyntaxError, TypeError):
111 _logger.debug('Invalid eval expression', exc_info=True)
114 _logger.debug('Disallowed or invalid eval expression', exc_info=True)
115 raise ValueError("%s is not a valid expression" % expr)
116 for code in _get_opcodes(code_obj):
117 if code not in allowed_codes:
118 raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
122 def const_eval(expr):
123 """const_eval(expression) -> value
125 Safe Python constant evaluation
127 Evaluates a string that contains an expression describing
128 a Python constant. Strings that are not valid Python expressions
129 or that contain other code besides the constant raise ValueError.
133 >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
134 [1, 2, (3, 4), {'foo': 'bar'}]
135 >>> const_eval("1+2")
136 Traceback (most recent call last):
138 ValueError: opcode BINARY_ADD not allowed
140 c = test_expr(expr, _CONST_OPCODES)
144 """expr_eval(expression) -> value
146 Restricted Python expression evaluation
148 Evaluates a string that contains an expression that only
149 uses Python constants. This can be used to e.g. evaluate
150 a numerical expression from an untrusted source.
154 >>> expr_eval("[1,2]*2")
156 >>> expr_eval("__import__('sys').modules")
157 Traceback (most recent call last):
159 ValueError: opcode LOAD_NAME not allowed
161 c = test_expr(expr, _EXPR_OPCODES)
165 # Port of Python 2.6's ast.literal_eval for use under Python 2.5
166 SAFE_CONSTANTS = {'None': None, 'True': True, 'False': False}
169 # first, try importing directly
170 from ast import literal_eval
175 if isinstance(node, ast.Str):
177 elif isinstance(node, ast.Num):
179 elif isinstance(node, ast.Tuple):
180 return tuple(map(_convert, node.elts))
181 elif isinstance(node, ast.List):
182 return list(map(_convert, node.elts))
183 elif isinstance(node, ast.Dict):
184 return dict((_convert(k), _convert(v)) for k, v
185 in zip(node.keys, node.values))
186 elif isinstance(node, ast.Name):
187 if node.id in SAFE_CONSTANTS:
188 return SAFE_CONSTANTS[node.id]
189 raise ValueError('malformed or disallowed expression')
191 def parse(expr, filename='<unknown>', mode='eval'):
192 """parse(source[, filename], mode]] -> code object
193 Parse an expression into an AST node.
194 Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
196 return compile(expr, filename, mode, ast.PyCF_ONLY_AST)
198 def literal_eval(node_or_string):
199 """literal_eval(expression) -> value
200 Safely evaluate an expression node or a string containing a Python
201 expression. The string or node provided may only consist of the
202 following Python literal structures: strings, numbers, tuples,
203 lists, dicts, booleans, and None.
205 >>> literal_eval('[1,True,"spam"]')
208 >>> literal_eval('1+3')
209 Traceback (most recent call last):
211 ValueError: malformed or disallowed expression
213 if isinstance(node_or_string, basestring):
214 node_or_string = parse(node_or_string)
215 if isinstance(node_or_string, ast.Expression):
216 node_or_string = node_or_string.body
217 return _convert(node_or_string)
219 def _import(name, globals=None, locals=None, fromlist=None, level=-1):
226 if name in _ALLOWED_MODULES:
227 return __import__(name, globals, locals, level)
228 raise ImportError(name)
230 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
231 """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
233 System-restricted Python expression evaluation
235 Evaluates a string that contains an expression that mostly
236 uses Python constants, arithmetic expressions and the
237 objects directly provided in context.
239 This can be used to e.g. evaluate
240 an OpenERP domain expression from an untrusted source.
242 Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
244 >>> safe_eval("__import__('sys').modules")
245 Traceback (most recent call last):
247 ValueError: opcode LOAD_NAME not allowed
250 if isinstance(expr, CodeType):
251 raise ValueError("safe_eval does not allow direct evaluation of code objects.")
253 if '__subclasses__' in expr:
254 raise ValueError('expression not allowed (__subclasses__)')
256 if globals_dict is None:
259 # prevent altering the globals/locals from within the sandbox
262 # isinstance() does not work below, we want *exactly* the dict class
263 if (globals_dict is not None and type(globals_dict) is not dict) \
264 or (locals_dict is not None and type(locals_dict) is not dict):
265 logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
266 "you should probably pass nocopy=True to safe_eval()')
268 globals_dict = dict(globals_dict)
269 if locals_dict is not None:
270 locals_dict = dict(locals_dict)
274 '__import__': _import,
296 return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
298 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: