[IMP] safe_eval: do not log exceptions, when re-raising a new exception, make the...
[odoo/odoo.git] / openerp / tools / safe_eval.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #    Copyright (C) 2004-2012 OpenERP s.a. (<http://www.openerp.com>).
4 #
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.
9 #
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.
14 #
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/>.
17 #
18 ##############################################################################
19
20 """
21 safe_eval module - methods intended to provide more restricted alternatives to
22                    evaluate simple and/or untrusted code.
23
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.
27 """
28
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
34 from opcode import HAVE_ARGUMENT, opmap, opname
35 from types import CodeType
36 import logging
37
38 __all__ = ['test_expr', 'safe_eval', 'const_eval']
39
40 # The time module is usually already provided in the safe_eval environment
41 # but some code, e.g. datetime.datetime.now() (Windows/Python 2.5.2, bug
42 # lp:703841), does import time.
43 _ALLOWED_MODULES = ['_strptime', 'time']
44
45 _CONST_OPCODES = set(opmap[x] for x in [
46     'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP', 'DUP_TOPX',
47     'POP_BLOCK','SETUP_LOOP', 'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
48     'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR', 'STORE_MAP'] if x in opmap)
49
50 _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
51     'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
52     'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY',
53     'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE',
54     'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR',
55     'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR',
56     'BINARY_OR', 'INPLACE_ADD', 'INPLACE_SUBTRACT', 'INPLACE_MULTIPLY',
57     'INPLACE_DIVIDE', 'INPLACE_REMAINDER', 'INPLACE_POWER',
58     'INPLACE_LEFTSHIFT', 'INPLACE_RIGHTSHIFT', 'INPLACE_AND',
59     'INPLACE_XOR','INPLACE_OR'
60     ] if x in opmap))
61
62 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
63     'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
64     'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
65     'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
66     'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
67     # New in Python 2.7 - http://bugs.python.org/issue4715 :
68     'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
69     'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY'
70     ] if x in opmap))
71
72 _logger = logging.getLogger(__name__)
73
74 def _get_opcodes(codeobj):
75     """_get_opcodes(codeobj) -> [opcodes]
76
77     Extract the actual opcodes as a list from a code object
78
79     >>> c = compile("[1 + 2, (1,2)]", "", "eval")
80     >>> _get_opcodes(c)
81     [100, 100, 23, 100, 100, 102, 103, 83]
82     """
83     i = 0
84     opcodes = []
85     byte_codes = codeobj.co_code
86     while i < len(byte_codes):
87         code = ord(byte_codes[i])
88         opcodes.append(code)
89         if code >= HAVE_ARGUMENT:
90             i += 3
91         else:
92             i += 1
93     return opcodes
94
95 def test_expr(expr, allowed_codes, mode="eval"):
96     """test_expr(expression, allowed_codes[, mode]) -> code_object
97
98     Test that the expression contains only the allowed opcodes.
99     If the expression is valid and contains only allowed codes,
100     return the compiled code object.
101     Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
102     """
103     try:
104         if mode == 'eval':
105             # eval() does not like leading/trailing whitespace
106             expr = expr.strip()
107         code_obj = compile(expr, "", mode)
108     except (SyntaxError, TypeError):
109         raise
110     except Exception, e:
111         import sys
112         exc_info = sys.exc_info()
113         raise ValueError, '"%s" while compiling\n%s' % (str(e), expr), exc_info[2]
114     for code in _get_opcodes(code_obj):
115         if code not in allowed_codes:
116             raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
117     return code_obj
118
119
120 def const_eval(expr):
121     """const_eval(expression) -> value
122
123     Safe Python constant evaluation
124
125     Evaluates a string that contains an expression describing
126     a Python constant. Strings that are not valid Python expressions
127     or that contain other code besides the constant raise ValueError.
128
129     >>> const_eval("10")
130     10
131     >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
132     [1, 2, (3, 4), {'foo': 'bar'}]
133     >>> const_eval("1+2")
134     Traceback (most recent call last):
135     ...
136     ValueError: opcode BINARY_ADD not allowed
137     """
138     c = test_expr(expr, _CONST_OPCODES)
139     return eval(c)
140
141 def expr_eval(expr):
142     """expr_eval(expression) -> value
143
144     Restricted Python expression evaluation
145
146     Evaluates a string that contains an expression that only
147     uses Python constants. This can be used to e.g. evaluate
148     a numerical expression from an untrusted source.
149
150     >>> expr_eval("1+2")
151     3
152     >>> expr_eval("[1,2]*2")
153     [1, 2, 1, 2]
154     >>> expr_eval("__import__('sys').modules")
155     Traceback (most recent call last):
156     ...
157     ValueError: opcode LOAD_NAME not allowed
158     """
159     c = test_expr(expr, _EXPR_OPCODES)
160     return eval(c)
161
162 def _import(name, globals=None, locals=None, fromlist=None, level=-1):
163     if globals is None:
164         globals = {}
165     if locals is None:
166         locals = {}
167     if fromlist is None:
168         fromlist = []
169     if name in _ALLOWED_MODULES:
170         return __import__(name, globals, locals, level)
171     raise ImportError(name)
172
173 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
174     """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
175
176     System-restricted Python expression evaluation
177
178     Evaluates a string that contains an expression that mostly
179     uses Python constants, arithmetic expressions and the
180     objects directly provided in context.
181
182     This can be used to e.g. evaluate
183     an OpenERP domain expression from an untrusted source.
184
185     Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
186
187     >>> safe_eval("__import__('sys').modules")
188     Traceback (most recent call last):
189     ...
190     ValueError: opcode LOAD_NAME not allowed
191
192     """
193     if isinstance(expr, CodeType):
194         raise ValueError("safe_eval does not allow direct evaluation of code objects.")
195
196     if '__subclasses__' in expr:
197        raise ValueError('expression not allowed (__subclasses__)')
198
199     if globals_dict is None:
200         globals_dict = {}
201
202     # prevent altering the globals/locals from within the sandbox
203     # by taking a copy.
204     if not nocopy:
205         # isinstance() does not work below, we want *exactly* the dict class
206         if (globals_dict is not None and type(globals_dict) is not dict) \
207             or (locals_dict is not None and type(locals_dict) is not dict):
208             _logger.warning(
209                 "Looks like you are trying to pass a dynamic environment, "
210                 "you should probably pass nocopy=True to safe_eval().")
211
212         globals_dict = dict(globals_dict)
213         if locals_dict is not None:
214             locals_dict = dict(locals_dict)
215
216     globals_dict.update(
217             __builtins__ = {
218                 '__import__': _import,
219                 'True': True,
220                 'False': False,
221                 'None': None,
222                 'str': str,
223                 'globals': locals,
224                 'locals': locals,
225                 'bool': bool,
226                 'dict': dict,
227                 'list': list,
228                 'tuple': tuple,
229                 'map': map,
230                 'abs': abs,
231                 'min': min,
232                 'max': max,
233                 'reduce': reduce,
234                 'filter': filter,
235                 'round': round,
236                 'len': len,
237                 'set' : set
238             }
239     )
240     c = test_expr(expr, _SAFE_OPCODES, mode=mode)
241     return eval(c, globals_dict, locals_dict)
242
243 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: