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 _ALLOWED_MODULES = ['_strptime']
44 _CONST_OPCODES = set(opmap[x] for x in [
45 'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP','POP_BLOCK','SETUP_LOOP',
46 'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
47 'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
49 _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
50 'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
51 'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY',
52 'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE',
53 'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR',
54 'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR',
55 'BINARY_OR'] if x in opmap))
57 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
58 'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
59 'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
60 'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
61 'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
62 # New in Python 2.7 - http://bugs.python.org/issue4715 :
63 'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
67 _logger = logging.getLogger('safe_eval')
69 def _get_opcodes(codeobj):
70 """_get_opcodes(codeobj) -> [opcodes]
72 Extract the actual opcodes as a list from a code object
74 >>> c = compile("[1 + 2, (1,2)]", "", "eval")
76 [100, 100, 23, 100, 100, 102, 103, 83]
80 byte_codes = codeobj.co_code
81 while i < len(byte_codes):
82 code = ord(byte_codes[i])
84 if code >= HAVE_ARGUMENT:
90 def test_expr(expr, allowed_codes, mode="eval"):
91 """test_expr(expression, allowed_codes[, mode]) -> code_object
93 Test that the expression contains only the allowed opcodes.
94 If the expression is valid and contains only allowed codes,
95 return the compiled code object.
96 Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
100 # eval() does not like leading/trailing whitespace
102 code_obj = compile(expr, "", mode)
103 except (SyntaxError, TypeError):
104 _logger.debug('Invalid eval expression', exc_info=True)
107 _logger.debug('Disallowed or invalid eval expression', exc_info=True)
108 raise ValueError("%s is not a valid expression" % expr)
109 for code in _get_opcodes(code_obj):
110 if code not in allowed_codes:
111 raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
115 def const_eval(expr):
116 """const_eval(expression) -> value
118 Safe Python constant evaluation
120 Evaluates a string that contains an expression describing
121 a Python constant. Strings that are not valid Python expressions
122 or that contain other code besides the constant raise ValueError.
126 >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
127 [1, 2, (3, 4), {'foo': 'bar'}]
128 >>> const_eval("1+2")
129 Traceback (most recent call last):
131 ValueError: opcode BINARY_ADD not allowed
133 c = test_expr(expr, _CONST_OPCODES)
137 """expr_eval(expression) -> value
139 Restricted Python expression evaluation
141 Evaluates a string that contains an expression that only
142 uses Python constants. This can be used to e.g. evaluate
143 a numerical expression from an untrusted source.
147 >>> expr_eval("[1,2]*2")
149 >>> expr_eval("__import__('sys').modules")
150 Traceback (most recent call last):
152 ValueError: opcode LOAD_NAME not allowed
154 c = test_expr(expr, _EXPR_OPCODES)
158 # Port of Python 2.6's ast.literal_eval for use under Python 2.5
159 SAFE_CONSTANTS = {'None': None, 'True': True, 'False': False}
162 # first, try importing directly
163 from ast import literal_eval
168 if isinstance(node, ast.Str):
170 elif isinstance(node, ast.Num):
172 elif isinstance(node, ast.Tuple):
173 return tuple(map(_convert, node.elts))
174 elif isinstance(node, ast.List):
175 return list(map(_convert, node.elts))
176 elif isinstance(node, ast.Dict):
177 return dict((_convert(k), _convert(v)) for k, v
178 in zip(node.keys, node.values))
179 elif isinstance(node, ast.Name):
180 if node.id in SAFE_CONSTANTS:
181 return SAFE_CONSTANTS[node.id]
182 raise ValueError('malformed or disallowed expression')
184 def parse(expr, filename='<unknown>', mode='eval'):
185 """parse(source[, filename], mode]] -> code object
186 Parse an expression into an AST node.
187 Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
189 return compile(expr, filename, mode, ast.PyCF_ONLY_AST)
191 def literal_eval(node_or_string):
192 """literal_eval(expression) -> value
193 Safely evaluate an expression node or a string containing a Python
194 expression. The string or node provided may only consist of the
195 following Python literal structures: strings, numbers, tuples,
196 lists, dicts, booleans, and None.
198 >>> literal_eval('[1,True,"spam"]')
201 >>> literal_eval('1+3')
202 Traceback (most recent call last):
204 ValueError: malformed or disallowed expression
206 if isinstance(node_or_string, basestring):
207 node_or_string = parse(node_or_string)
208 if isinstance(node_or_string, ast.Expression):
209 node_or_string = node_or_string.body
210 return _convert(node_or_string)
212 def _import(name, globals={}, locals={}, fromlist=[], level=-1):
213 if name in _ALLOWED_MODULES:
214 return __import__(name, globals, locals, level)
215 raise ImportError(name)
217 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
218 """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
220 System-restricted Python expression evaluation
222 Evaluates a string that contains an expression that mostly
223 uses Python constants, arithmetic expressions and the
224 objects directly provided in context.
226 This can be used to e.g. evaluate
227 an OpenERP domain expression from an untrusted source.
229 Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
231 >>> safe_eval("__import__('sys').modules")
232 Traceback (most recent call last):
234 ValueError: opcode LOAD_NAME not allowed
237 if isinstance(expr, CodeType):
238 raise ValueError("safe_eval does not allow direct evaluation of code objects.")
240 if '__subclasses__' in expr:
241 raise ValueError('expression not allowed (__subclasses__)')
243 if globals_dict is None:
246 # prevent altering the globals/locals from within the sandbox
249 # isinstance() does not work below, we want *exactly* the dict class
250 if (globals_dict is not None and type(globals_dict) is not dict) \
251 or (locals_dict is not None and type(locals_dict) is not dict):
252 logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
253 "you should probably pass nocopy=True to safe_eval()')
255 globals_dict = dict(globals_dict)
256 if locals_dict is not None:
257 locals_dict = dict(locals_dict)
261 '__import__': _import,
281 return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
283 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: