[FIX] Allow missing opcodes, harden check for private attributes (dunder), check...
[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
34 from opcode import HAVE_ARGUMENT, opmap, opname
35 from types import CodeType
36 import logging
37 import os
38
39 __all__ = ['test_expr', 'safe_eval', 'const_eval']
40
41 # The time module is usually already provided in the safe_eval environment
42 # but some code, e.g. datetime.datetime.now() (Windows/Python 2.5.2, bug
43 # lp:703841), does import time.
44 _ALLOWED_MODULES = ['_strptime', 'time']
45
46 _CONST_OPCODES = set(opmap[x] for x in [
47     'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP', 'DUP_TOPX',
48     'POP_BLOCK','SETUP_LOOP', 'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
49     'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
50
51 _EXPR_OPCODES = _CONST_OPCODES.union(set(opmap[x] for x in [
52     'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT',
53     'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY',
54     'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE',
55     'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR',
56     'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR',
57     'BINARY_OR', 'INPLACE_ADD', 'INPLACE_SUBTRACT', 'INPLACE_MULTIPLY',
58     'INPLACE_DIVIDE', 'INPLACE_REMAINDER', 'INPLACE_POWER',
59     'INPLACE_LEFTSHIFT', 'INPLACE_RIGHTSHIFT', 'INPLACE_AND',
60     'INPLACE_XOR','INPLACE_OR'
61     ] if x in opmap))
62
63 _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
64     'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR',
65     'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
66     'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
67     'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
68     # New in Python 2.7 - http://bugs.python.org/issue4715 :
69     'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
70     'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY', 'LOAD_FAST',
71     'LOAD_GLOBAL',
72     ] if x in opmap))
73
74 _logger = logging.getLogger(__name__)
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     byte_codes = codeobj.co_code
87     while i < len(byte_codes):
88         code = ord(byte_codes[i])
89         yield code
90
91         if code >= HAVE_ARGUMENT:
92             i += 3
93         else:
94             i += 1
95
96 def assert_no_dunder_name(code_obj, expr):
97     """ assert_no_dunder_name(code_obj, expr) -> None
98
99     Asserts that the code object does not refer to any "dunder name"
100     (__$name__), so that safe_eval prevents access to any internal-ish Python
101     attribute or method (both are loaded via LOAD_ATTR which uses a name, not a
102     const or a var).
103
104     Checks that no such name exists in the provided code object (co_names).
105
106     :param code_obj: code object to name-validate
107     :type code_obj: CodeType
108     :param str expr: expression corresponding to the code object, for debugging
109                      purposes
110     :raises NameError: in case a forbidden name (containing two underscores)
111                        is found in ``code_obj``
112
113     .. note:: actually forbids every name containing 2 underscores
114     """
115     for name in code_obj.co_names:
116         if "__" in name:
117             raise NameError('Access to forbidden name %r (%r)' % (name, expr))
118
119 def assert_valid_codeobj(allowed_codes, code_obj, expr):
120     """ Asserts that the provided code object validates against the bytecode
121     and name constraints.
122
123     Recursively validates the code objects stored in its co_consts in case
124     lambdas are being created/used (lambdas generate their own separated code
125     objects and don't live in the root one)
126
127     :param allowed_codes: list of permissible bytecode instructions
128     :type allowed_codes: set(int)
129     :param code_obj: code object to name-validate
130     :type code_obj: CodeType
131     :param str expr: expression corresponding to the code object, for debugging
132                      purposes
133     :raises ValueError: in case of forbidden bytecode in ``code_obj``
134     :raises NameError: in case a forbidden name (containing two underscores)
135                        is found in ``code_obj``
136     """
137     assert_no_dunder_name(code_obj, expr)
138     for opcode in _get_opcodes(code_obj):
139         if opcode not in allowed_codes:
140             raise ValueError(
141                 "opcode %s not allowed (%r)" % (opname[opcode], expr))
142     for const in code_obj.co_consts:
143         if isinstance(const, CodeType):
144             assert_valid_codeobj(allowed_codes, const, 'lambda')
145
146 def test_expr(expr, allowed_codes, mode="eval"):
147     """test_expr(expression, allowed_codes[, mode]) -> code_object
148
149     Test that the expression contains only the allowed opcodes.
150     If the expression is valid and contains only allowed codes,
151     return the compiled code object.
152     Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
153     """
154     try:
155         if mode == 'eval':
156             # eval() does not like leading/trailing whitespace
157             expr = expr.strip()
158         code_obj = compile(expr, "", mode)
159     except (SyntaxError, TypeError, ValueError):
160         _logger.debug('Invalid eval expression', exc_info=True)
161         raise
162     except Exception:
163         _logger.debug('Disallowed or invalid eval expression', exc_info=True)
164         raise ValueError("%s is not a valid expression" % expr)
165
166     assert_valid_codeobj(allowed_codes, code_obj, expr)
167     return code_obj
168
169
170 def const_eval(expr):
171     """const_eval(expression) -> value
172
173     Safe Python constant evaluation
174
175     Evaluates a string that contains an expression describing
176     a Python constant. Strings that are not valid Python expressions
177     or that contain other code besides the constant raise ValueError.
178
179     >>> const_eval("10")
180     10
181     >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
182     [1, 2, (3, 4), {'foo': 'bar'}]
183     >>> const_eval("1+2")
184     Traceback (most recent call last):
185     ...
186     ValueError: opcode BINARY_ADD not allowed
187     """
188     c = test_expr(expr, _CONST_OPCODES)
189     return eval(c)
190
191 def expr_eval(expr):
192     """expr_eval(expression) -> value
193
194     Restricted Python expression evaluation
195
196     Evaluates a string that contains an expression that only
197     uses Python constants. This can be used to e.g. evaluate
198     a numerical expression from an untrusted source.
199
200     >>> expr_eval("1+2")
201     3
202     >>> expr_eval("[1,2]*2")
203     [1, 2, 1, 2]
204     >>> expr_eval("__import__('sys').modules")
205     Traceback (most recent call last):
206     ...
207     ValueError: opcode LOAD_NAME not allowed
208     """
209     c = test_expr(expr, _EXPR_OPCODES)
210     return eval(c)
211
212 def _import(name, globals=None, locals=None, fromlist=None, level=-1):
213     if globals is None:
214         globals = {}
215     if locals is None:
216         locals = {}
217     if fromlist is None:
218         fromlist = []
219     if name in _ALLOWED_MODULES:
220         return __import__(name, globals, locals, level)
221     raise ImportError(name)
222
223 def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
224     """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
225
226     System-restricted Python expression evaluation
227
228     Evaluates a string that contains an expression that mostly
229     uses Python constants, arithmetic expressions and the
230     objects directly provided in context.
231
232     This can be used to e.g. evaluate
233     an OpenERP domain expression from an untrusted source.
234
235     :throws TypeError: If the expression provided is a code object
236     :throws SyntaxError: If the expression provided is not valid Python
237     :throws NameError: If the expression provided accesses forbidden names
238     :throws ValueError: If the expression provided uses forbidden bytecode
239     """
240     if isinstance(expr, CodeType):
241         raise TypeError("safe_eval does not allow direct evaluation of code objects.")
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             _logger.warning(
253                 "Looks like you are trying to pass a dynamic environment, "
254                 "you should probably pass nocopy=True to safe_eval().")
255
256         globals_dict = dict(globals_dict)
257         if locals_dict is not None:
258             locals_dict = dict(locals_dict)
259
260     globals_dict.update(
261             __builtins__ = {
262                 '__import__': _import,
263                 'True': True,
264                 'False': False,
265                 'None': None,
266                 'str': str,
267                 'globals': locals,
268                 'locals': locals,
269                 'bool': bool,
270                 'dict': dict,
271                 'list': list,
272                 'tuple': tuple,
273                 'map': map,
274                 'abs': abs,
275                 'min': min,
276                 'max': max,
277                 'reduce': reduce,
278                 'filter': filter,
279                 'round': round,
280                 'len': len,
281                 'set' : set
282             }
283     )
284     return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
285
286 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: