[IMP] safe_eval: allow INPLACE* operator opcodes as introduced in PEP-203
[odoo/odoo.git] / openerp / tools / safe_eval.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #    Copyright (C) 2004-2010 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 #  - python 2.6's ast.literal_eval
34
35 from opcode import HAVE_ARGUMENT, opmap, opname
36 from types import CodeType
37 import logging
38 import os
39
40 __all__ = ['test_expr', 'literal_eval', 'safe_eval', 'const_eval', 'ext_eval' ]
41
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']
46
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)
51
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'
62     ] if x in opmap))
63
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'
72     ] if x in opmap))
73
74 _logger = logging.getLogger('safe_eval')
75
76 def _get_opcodes(codeobj):
77     """_get_opcodes(codeobj) -> [opcodes]
78
79     Extract the actual opcodes as a list from a code object
80
81     >>> c = compile("[1 + 2, (1,2)]", "", "eval")
82     >>> _get_opcodes(c)
83     [100, 100, 23, 100, 100, 102, 103, 83]
84     """
85     i = 0
86     opcodes = []
87     byte_codes = codeobj.co_code
88     while i < len(byte_codes):
89         code = ord(byte_codes[i])
90         opcodes.append(code)
91         if code >= HAVE_ARGUMENT:
92             i += 3
93         else:
94             i += 1
95     return opcodes
96
97 def test_expr(expr, allowed_codes, mode="eval"):
98     """test_expr(expression, allowed_codes[, mode]) -> code_object
99
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.
104     """
105     try:
106         if mode == 'eval':
107             # eval() does not like leading/trailing whitespace
108             expr = expr.strip()
109         code_obj = compile(expr, "", mode)
110     except (SyntaxError, TypeError):
111         _logger.debug('Invalid eval expression', exc_info=True)
112         raise
113     except Exception:
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))
119     return code_obj
120
121
122 def const_eval(expr):
123     """const_eval(expression) -> value
124
125     Safe Python constant evaluation
126
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.
130
131     >>> const_eval("10")
132     10
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):
137     ...
138     ValueError: opcode BINARY_ADD not allowed
139     """
140     c = test_expr(expr, _CONST_OPCODES)
141     return eval(c)
142
143 def expr_eval(expr):
144     """expr_eval(expression) -> value
145
146     Restricted Python expression evaluation
147
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.
151
152     >>> expr_eval("1+2")
153     3
154     >>> expr_eval("[1,2]*2")
155     [1, 2, 1, 2]
156     >>> expr_eval("__import__('sys').modules")
157     Traceback (most recent call last):
158     ...
159     ValueError: opcode LOAD_NAME not allowed
160     """
161     c = test_expr(expr, _EXPR_OPCODES)
162     return eval(c)
163
164
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}
167
168 try:
169     # first, try importing directly
170     from ast import literal_eval
171 except ImportError:
172     import _ast as ast
173
174     def _convert(node):
175         if isinstance(node, ast.Str):
176             return node.s
177         elif isinstance(node, ast.Num):
178             return node.n
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')
190
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).
195         """
196         return compile(expr, filename, mode, ast.PyCF_ONLY_AST)
197
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.
204
205         >>> literal_eval('[1,True,"spam"]')
206         [1, True, 'spam']
207
208         >>> literal_eval('1+3')
209         Traceback (most recent call last):
210         ...
211         ValueError: malformed or disallowed expression
212         """
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)
218
219 def _import(name, globals=None, locals=None, fromlist=None, level=-1):
220     if globals is None:
221         globals = {}
222     if locals is None:
223         locals = {}
224     if fromlist is None:
225         fromlist = []
226     if name in _ALLOWED_MODULES:
227         return __import__(name, globals, locals, level)
228     raise ImportError(name)
229
230 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
231     """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
232
233     System-restricted Python expression evaluation
234
235     Evaluates a string that contains an expression that mostly
236     uses Python constants, arithmetic expressions and the
237     objects directly provided in context.
238
239     This can be used to e.g. evaluate
240     an OpenERP domain expression from an untrusted source.
241
242     Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
243
244     >>> safe_eval("__import__('sys').modules")
245     Traceback (most recent call last):
246     ...
247     ValueError: opcode LOAD_NAME not allowed
248
249     """
250     if isinstance(expr, CodeType):
251         raise ValueError("safe_eval does not allow direct evaluation of code objects.")
252
253     if '__subclasses__' in expr:
254        raise ValueError('expression not allowed (__subclasses__)')
255
256     if globals_dict is None:
257         globals_dict = {}
258
259     # prevent altering the globals/locals from within the sandbox
260     # by taking a copy.
261     if not nocopy:
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()')
267
268         globals_dict = dict(globals_dict)
269         if locals_dict is not None:
270             locals_dict = dict(locals_dict)
271
272     globals_dict.update(
273             __builtins__ = {
274                 '__import__': _import,
275                 'True': True,
276                 'False': False,
277                 'None': None,
278                 'str': str,
279                 'globals': locals,
280                 'locals': locals,
281                 'bool': bool,
282                 'dict': dict,
283                 'list': list,
284                 'tuple': tuple,
285                 'map': map,
286                 'abs': abs,
287                 'min': min,
288                 'max': max,
289                 'reduce': reduce,
290                 'filter': filter,
291                 'round': round,
292                 'len': len,
293                 'set' : set
294             }
295     )
296     return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
297
298 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: