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