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