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