[FIX] Safe_eval : Added the support of set(),JUMP_FORWARD,POP_BLOCK,SETUP_LOOP
[odoo/odoo.git] / bin / 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 _CONST_OPCODES = set(opmap[x] for x in [
43     'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP','POP_BLOCK','SETUP_LOOP',
44     'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
45     'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
46
47 _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
48     'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
49     'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY',
50     'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE',
51     'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR',
52     'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR',
53     'BINARY_OR'] if x in opmap))
54
55 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
56     'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
57     'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'JUMP_ABSOLUTE',
58     'DELETE_NAME', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE','MAKE_FUNCTION','JUMP_FORWARD'
59     ] if x in opmap))
60
61 _logger = logging.getLogger('safe_eval')
62
63 def _get_opcodes(codeobj):
64     """_get_opcodes(codeobj) -> [opcodes]
65
66     Extract the actual opcodes as a list from a code object
67
68     >>> c = compile("[1 + 2, (1,2)]", "", "eval")
69     >>> _get_opcodes(c)
70     [100, 100, 23, 100, 100, 102, 103, 83]
71     """
72     i = 0
73     opcodes = []
74     byte_codes = codeobj.co_code
75     while i < len(byte_codes):
76         code = ord(byte_codes[i])
77         opcodes.append(code)
78         if code >= HAVE_ARGUMENT:
79             i += 3
80         else:
81             i += 1
82     return opcodes
83
84 def test_expr(expr, allowed_codes, mode="eval"):
85     """test_expr(expression, allowed_codes[, mode]) -> code_object
86
87     Test that the expression contains only the allowed opcodes.
88     If the expression is valid and contains only allowed codes,
89     return the compiled code object.
90     Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
91     """
92     try:
93         if mode == 'eval':
94             # eval() does not like leading/trailing whitespace
95             expr = expr.strip()
96         code_obj = compile(expr, "", mode)
97     except (SyntaxError, TypeError):
98         _logger.debug('Invalid eval expression', exc_info=True)
99         raise
100     except Exception:
101         _logger.debug('Disallowed or invalid eval expression', exc_info=True)
102         raise ValueError("%s is not a valid expression" % expr)
103     for code in _get_opcodes(code_obj):
104         if code not in allowed_codes:
105             raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr))
106     return code_obj
107
108
109 def const_eval(expr):
110     """const_eval(expression) -> value
111
112     Safe Python constant evaluation
113
114     Evaluates a string that contains an expression describing
115     a Python constant. Strings that are not valid Python expressions
116     or that contain other code besides the constant raise ValueError.
117
118     >>> const_eval("10")
119     10
120     >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
121     [1, 2, (3, 4), {'foo': 'bar'}]
122     >>> const_eval("1+2")
123     Traceback (most recent call last):
124     ...
125     ValueError: opcode BINARY_ADD not allowed
126     """
127     c = test_expr(expr, _CONST_OPCODES)
128     return eval(c)
129
130 def expr_eval(expr):
131     """expr_eval(expression) -> value
132
133     Restricted Python expression evaluation
134
135     Evaluates a string that contains an expression that only
136     uses Python constants. This can be used to e.g. evaluate
137     a numerical expression from an untrusted source.
138
139     >>> expr_eval("1+2")
140     3
141     >>> expr_eval("[1,2]*2")
142     [1, 2, 1, 2]
143     >>> expr_eval("__import__('sys').modules")
144     Traceback (most recent call last):
145     ...
146     ValueError: opcode LOAD_NAME not allowed
147     """
148     c = test_expr(expr, _EXPR_OPCODES)
149     return eval(c)
150
151
152 # Port of Python 2.6's ast.literal_eval for use under Python 2.5
153 SAFE_CONSTANTS = {'None': None, 'True': True, 'False': False}
154
155 try:
156     # first, try importing directly
157     from ast import literal_eval
158 except ImportError:
159     import _ast as ast
160
161     def _convert(node):
162         if isinstance(node, ast.Str):
163             return node.s
164         elif isinstance(node, ast.Num):
165             return node.n
166         elif isinstance(node, ast.Tuple):
167             return tuple(map(_convert, node.elts))
168         elif isinstance(node, ast.List):
169             return list(map(_convert, node.elts))
170         elif isinstance(node, ast.Dict):
171             return dict((_convert(k), _convert(v)) for k, v
172                         in zip(node.keys, node.values))
173         elif isinstance(node, ast.Name):
174             if node.id in SAFE_CONSTANTS:
175                 return SAFE_CONSTANTS[node.id]
176         raise ValueError('malformed or disallowed expression')
177
178     def parse(expr, filename='<unknown>', mode='eval'):
179         """parse(source[, filename], mode]] -> code object
180         Parse an expression into an AST node.
181         Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
182         """
183         return compile(expr, filename, mode, ast.PyCF_ONLY_AST)
184
185     def literal_eval(node_or_string):
186         """literal_eval(expression) -> value
187         Safely evaluate an expression node or a string containing a Python
188         expression.  The string or node provided may only consist of the
189         following Python literal structures: strings, numbers, tuples,
190         lists, dicts, booleans, and None.
191
192         >>> literal_eval('[1,True,"spam"]')
193         [1, True, 'spam']
194
195         >>> literal_eval('1+3')
196         Traceback (most recent call last):
197         ...
198         ValueError: malformed or disallowed expression
199         """
200         if isinstance(node_or_string, basestring):
201             node_or_string = parse(node_or_string)
202         if isinstance(node_or_string, ast.Expression):
203             node_or_string = node_or_string.body
204         return _convert(node_or_string)
205
206
207
208 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
209     """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
210
211     System-restricted Python expression evaluation
212
213     Evaluates a string that contains an expression that mostly
214     uses Python constants, arithmetic expressions and the
215     objects directly provided in context.
216
217     This can be used to e.g. evaluate
218     an OpenERP domain expression from an untrusted source.
219
220     Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
221
222     >>> safe_eval("__import__('sys').modules")
223     Traceback (most recent call last):
224     ...
225     ValueError: opcode LOAD_NAME not allowed
226
227     """
228     if isinstance(expr, CodeType):
229         raise ValueError("safe_eval does not allow direct evaluation of code objects.")
230
231     if '__subclasses__' in expr:
232        raise ValueError('expression not allowed (__subclasses__)')
233
234     if globals_dict is None:
235         globals_dict = {}
236
237     # prevent altering the globals/locals from within the sandbox
238     # by taking a copy.
239     if not nocopy:
240         # isinstance() does not work below, we want *exactly* the dict class
241         if (globals_dict is not None and type(globals_dict) is not dict) \
242             or (locals_dict is not None and type(locals_dict) is not dict):
243             logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
244                               "you should probably pass nocopy=True to safe_eval()')
245
246         globals_dict = dict(globals_dict)
247         if locals_dict is not None:
248             locals_dict = dict(locals_dict)
249
250     globals_dict.update(
251             __builtins__ = {
252                 'True': True,
253                 'False': False,
254                 'None': None,
255                 'str': str,
256                 'globals': locals,
257                 'locals': locals,
258                 'bool': bool,
259                 'dict': dict,
260                 'list': list,
261                 'tuple': tuple,
262                 'map': map,
263                 'abs': abs,
264                 'reduce': reduce,
265                 'filter': filter,
266                 'round': round,
267                 'len': len,
268                 'set' : set
269             }
270     )
271     return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
272
273 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: