[FIX] @charset in CSS files
[odoo/odoo.git] / openerp / tools / safe_eval.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #    Copyright (C) 2004-2014 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', 'LOAD_FAST',
75     'LOAD_GLOBAL', # Only allows access to restricted globals
76     ] if x in opmap))
77
78 _logger = logging.getLogger(__name__)
79
80 def _get_opcodes(codeobj):
81     """_get_opcodes(codeobj) -> [opcodes]
82
83     Extract the actual opcodes as a list from a code object
84
85     >>> c = compile("[1 + 2, (1,2)]", "", "eval")
86     >>> _get_opcodes(c)
87     [100, 100, 23, 100, 100, 102, 103, 83]
88     """
89     i = 0
90     byte_codes = codeobj.co_code
91     while i < len(byte_codes):
92         code = ord(byte_codes[i])
93         yield code
94
95         if code >= HAVE_ARGUMENT:
96             i += 3
97         else:
98             i += 1
99
100 def assert_no_dunder_name(code_obj, expr):
101     """ assert_no_dunder_name(code_obj, expr) -> None
102
103     Asserts that the code object does not refer to any "dunder name"
104     (__$name__), so that safe_eval prevents access to any internal-ish Python
105     attribute or method (both are loaded via LOAD_ATTR which uses a name, not a
106     const or a var).
107
108     Checks that no such name exists in the provided code object (co_names).
109
110     :param code_obj: code object to name-validate
111     :type code_obj: CodeType
112     :param str expr: expression corresponding to the code object, for debugging
113                      purposes
114     :raises NameError: in case a forbidden name (containing two underscores)
115                        is found in ``code_obj``
116
117     .. note:: actually forbids every name containing 2 underscores
118     """
119     for name in code_obj.co_names:
120         if "__" in name:
121             raise NameError('Access to forbidden name %r (%r)' % (name, expr))
122
123 def assert_valid_codeobj(allowed_codes, code_obj, expr):
124     """ Asserts that the provided code object validates against the bytecode
125     and name constraints.
126
127     Recursively validates the code objects stored in its co_consts in case
128     lambdas are being created/used (lambdas generate their own separated code
129     objects and don't live in the root one)
130
131     :param allowed_codes: list of permissible bytecode instructions
132     :type allowed_codes: set(int)
133     :param code_obj: code object to name-validate
134     :type code_obj: CodeType
135     :param str expr: expression corresponding to the code object, for debugging
136                      purposes
137     :raises ValueError: in case of forbidden bytecode in ``code_obj``
138     :raises NameError: in case a forbidden name (containing two underscores)
139                        is found in ``code_obj``
140     """
141     assert_no_dunder_name(code_obj, expr)
142     for opcode in _get_opcodes(code_obj):
143         if opcode not in allowed_codes:
144             raise ValueError(
145                 "opcode %s not allowed (%r)" % (opname[opcode], expr))
146     for const in code_obj.co_consts:
147         if isinstance(const, CodeType):
148             assert_valid_codeobj(allowed_codes, const, 'lambda')
149
150 def test_expr(expr, allowed_codes, mode="eval"):
151     """test_expr(expression, allowed_codes[, mode]) -> code_object
152
153     Test that the expression contains only the allowed opcodes.
154     If the expression is valid and contains only allowed codes,
155     return the compiled code object.
156     Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
157     """
158     try:
159         if mode == 'eval':
160             # eval() does not like leading/trailing whitespace
161             expr = expr.strip()
162         code_obj = compile(expr, "", mode)
163     except (SyntaxError, TypeError, ValueError):
164         raise
165     except Exception, e:
166         import sys
167         exc_info = sys.exc_info()
168         raise ValueError, '"%s" while compiling\n%r' % (ustr(e), expr), exc_info[2]
169     assert_valid_codeobj(allowed_codes, code_obj, expr)
170     return code_obj
171
172
173 def const_eval(expr):
174     """const_eval(expression) -> value
175
176     Safe Python constant evaluation
177
178     Evaluates a string that contains an expression describing
179     a Python constant. Strings that are not valid Python expressions
180     or that contain other code besides the constant raise ValueError.
181
182     >>> const_eval("10")
183     10
184     >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
185     [1, 2, (3, 4), {'foo': 'bar'}]
186     >>> const_eval("1+2")
187     Traceback (most recent call last):
188     ...
189     ValueError: opcode BINARY_ADD not allowed
190     """
191     c = test_expr(expr, _CONST_OPCODES)
192     return eval(c)
193
194 def expr_eval(expr):
195     """expr_eval(expression) -> value
196
197     Restricted Python expression evaluation
198
199     Evaluates a string that contains an expression that only
200     uses Python constants. This can be used to e.g. evaluate
201     a numerical expression from an untrusted source.
202
203     >>> expr_eval("1+2")
204     3
205     >>> expr_eval("[1,2]*2")
206     [1, 2, 1, 2]
207     >>> expr_eval("__import__('sys').modules")
208     Traceback (most recent call last):
209     ...
210     ValueError: opcode LOAD_NAME not allowed
211     """
212     c = test_expr(expr, _EXPR_OPCODES)
213     return eval(c)
214
215 def _import(name, globals=None, locals=None, fromlist=None, level=-1):
216     if globals is None:
217         globals = {}
218     if locals is None:
219         locals = {}
220     if fromlist is None:
221         fromlist = []
222     if name in _ALLOWED_MODULES:
223         return __import__(name, globals, locals, level)
224     raise ImportError(name)
225
226 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False, locals_builtins=False):
227     """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
228
229     System-restricted Python expression evaluation
230
231     Evaluates a string that contains an expression that mostly
232     uses Python constants, arithmetic expressions and the
233     objects directly provided in context.
234
235     This can be used to e.g. evaluate
236     an OpenERP domain expression from an untrusted source.
237
238     :throws TypeError: If the expression provided is a code object
239     :throws SyntaxError: If the expression provided is not valid Python
240     :throws NameError: If the expression provided accesses forbidden names
241     :throws ValueError: If the expression provided uses forbidden bytecode
242     """
243     if isinstance(expr, CodeType):
244         raise TypeError("safe_eval does not allow direct evaluation of code objects.")
245
246     if globals_dict is None:
247         globals_dict = {}
248
249     # prevent altering the globals/locals from within the sandbox
250     # by taking a copy.
251     if not nocopy:
252         # isinstance() does not work below, we want *exactly* the dict class
253         if (globals_dict is not None and type(globals_dict) is not dict) \
254             or (locals_dict is not None and type(locals_dict) is not dict):
255             _logger.warning(
256                 "Looks like you are trying to pass a dynamic environment, "
257                 "you should probably pass nocopy=True to safe_eval().")
258
259         globals_dict = dict(globals_dict)
260         if locals_dict is not None:
261             locals_dict = dict(locals_dict)
262
263     globals_dict.update(
264         __builtins__={
265             '__import__': _import,
266             'True': True,
267             'False': False,
268             'None': None,
269             'str': str,
270             'unicode': unicode,
271             'globals': locals,
272             'locals': locals,
273             'bool': bool,
274             'int': int,
275             'float': float,
276             'long': long,
277             'enumerate': enumerate,
278             'dict': dict,
279             'list': list,
280             'tuple': tuple,
281             'map': map,
282             'abs': abs,
283             'min': min,
284             'max': max,
285             'sum': sum,
286             'reduce': reduce,
287             'filter': filter,
288             'round': round,
289             'len': len,
290             'repr': repr,
291             'set': set,
292             'all': all,
293             'any': any,
294             'ord': ord,
295             'chr': chr,
296             'cmp': cmp,
297             'divmod': divmod,
298             'isinstance': isinstance,
299             'range': range,
300             'xrange': xrange,
301             'zip': zip,
302         }
303     )
304     if locals_builtins:
305         if locals_dict is None:
306             locals_dict = {}
307         locals_dict.update(globals_dict.get('__builtins__'))
308     c = test_expr(expr, _SAFE_OPCODES, mode=mode)
309     try:
310         return eval(c, globals_dict, locals_dict)
311     except openerp.osv.orm.except_orm:
312         raise
313     except openerp.exceptions.Warning:
314         raise
315     except openerp.exceptions.RedirectWarning:
316         raise
317     except openerp.exceptions.AccessDenied:
318         raise
319     except openerp.exceptions.AccessError:
320         raise
321     except Exception, e:
322         import sys
323         exc_info = sys.exc_info()
324         raise ValueError, '"%s" while evaluating\n%r' % (ustr(e), expr), exc_info[2]
325
326 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: