1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4"""
5safe_eval module - methods intended to provide more restricted alternatives to
6                   evaluate simple and/or untrusted code.
7
8Methods in this module are typically used as alternatives to eval() to parse
9OpenERP domain strings, conditions and expressions, mostly based on locals
10condition/math builtins.
11"""
12
13# Module partially ripped from/inspired by several different sources:
14#  - http://code.activestate.com/recipes/286134/
15#  - safe_eval in lp:~xrg/openobject-server/optimize-5.0
16#  - safe_eval in tryton http://hg.tryton.org/hgwebdir.cgi/trytond/rev/bbb5f73319ad
17import dis
18import functools
19import logging
20import types
21from opcode import HAVE_ARGUMENT, opmap, opname
22from types import CodeType
23
24import werkzeug
25from psycopg2 import OperationalError
26
27from .misc import ustr
28
29import odoo
30
31unsafe_eval = eval
32
33__all__ = ['test_expr', 'safe_eval', 'const_eval']
34
35# The time module is usually already provided in the safe_eval environment
36# but some code, e.g. datetime.datetime.now() (Windows/Python 2.5.2, bug
37# lp:703841), does import time.
38_ALLOWED_MODULES = ['_strptime', 'math', 'time']
39
40_UNSAFE_ATTRIBUTES = ['f_builtins', 'f_globals', 'f_locals', 'gi_frame',
41                      'co_code', 'func_globals']
42
43def to_opcodes(opnames, _opmap=opmap):
44    for x in opnames:
45        if x in _opmap:
46            yield _opmap[x]
47# opcodes which absolutely positively must not be usable in safe_eval,
48# explicitly subtracted from all sets of valid opcodes just in case
49_BLACKLIST = set(to_opcodes([
50    # can't provide access to accessing arbitrary modules
51    'IMPORT_STAR', 'IMPORT_NAME', 'IMPORT_FROM',
52    # could allow replacing or updating core attributes on models & al, setitem
53    # can be used to set field values
54    'STORE_ATTR', 'DELETE_ATTR',
55    # no reason to allow this
56    'STORE_GLOBAL', 'DELETE_GLOBAL',
57]))
58# opcodes necessary to build literal values
59_CONST_OPCODES = set(to_opcodes([
60    # stack manipulations
61    'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP', 'DUP_TOP_TWO',
62    'LOAD_CONST',
63    'RETURN_VALUE', # return the result of the literal/expr evaluation
64    # literal collections
65    'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE', 'BUILD_SET',
66    # 3.6: literal map with constant keys https://bugs.python.org/issue27140
67    'BUILD_CONST_KEY_MAP',
68    'LIST_EXTEND', 'SET_UPDATE',
69])) - _BLACKLIST
70
71# operations which are both binary and inplace, same order as in doc'
72_operations = [
73    'POWER', 'MULTIPLY', # 'MATRIX_MULTIPLY', # matrix operator (3.5+)
74    'FLOOR_DIVIDE', 'TRUE_DIVIDE', 'MODULO', 'ADD',
75    'SUBTRACT', 'LSHIFT', 'RSHIFT', 'AND', 'XOR', 'OR',
76]
77# operations on literal values
78_EXPR_OPCODES = _CONST_OPCODES.union(to_opcodes([
79    'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT', 'UNARY_INVERT',
80    *('BINARY_' + op for op in _operations), 'BINARY_SUBSCR',
81    *('INPLACE_' + op for op in _operations),
82    'BUILD_SLICE',
83    # comprehensions
84    'LIST_APPEND', 'MAP_ADD', 'SET_ADD',
85    'COMPARE_OP',
86    # specialised comparisons
87    'IS_OP', 'CONTAINS_OP',
88    'DICT_MERGE', 'DICT_UPDATE',
89])) - _BLACKLIST
90
91_SAFE_OPCODES = _EXPR_OPCODES.union(to_opcodes([
92    'POP_BLOCK', 'POP_EXCEPT',
93
94    # note: removed in 3.8
95    'SETUP_LOOP', 'SETUP_EXCEPT', 'BREAK_LOOP', 'CONTINUE_LOOP',
96
97    'EXTENDED_ARG',  # P3.6 for long jump offsets.
98    'MAKE_FUNCTION', 'CALL_FUNCTION', 'CALL_FUNCTION_KW', 'CALL_FUNCTION_EX',
99    # Added in P3.7 https://bugs.python.org/issue26110
100    'CALL_METHOD', 'LOAD_METHOD',
101
102    'GET_ITER', 'FOR_ITER', 'YIELD_VALUE',
103    'JUMP_FORWARD', 'JUMP_ABSOLUTE',
104    'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE', 'POP_JUMP_IF_TRUE',
105    'SETUP_FINALLY', 'END_FINALLY',
106    # Added in 3.8 https://bugs.python.org/issue17611
107    'BEGIN_FINALLY', 'CALL_FINALLY', 'POP_FINALLY',
108
109    'RAISE_VARARGS', 'LOAD_NAME', 'STORE_NAME', 'DELETE_NAME', 'LOAD_ATTR',
110    'LOAD_FAST', 'STORE_FAST', 'DELETE_FAST', 'UNPACK_SEQUENCE',
111    'STORE_SUBSCR',
112    'LOAD_GLOBAL',
113
114    'RERAISE', 'JUMP_IF_NOT_EXC_MATCH',
115])) - _BLACKLIST
116
117_logger = logging.getLogger(__name__)
118
119def assert_no_dunder_name(code_obj, expr):
120    """ assert_no_dunder_name(code_obj, expr) -> None
121
122    Asserts that the code object does not refer to any "dunder name"
123    (__$name__), so that safe_eval prevents access to any internal-ish Python
124    attribute or method (both are loaded via LOAD_ATTR which uses a name, not a
125    const or a var).
126
127    Checks that no such name exists in the provided code object (co_names).
128
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 NameError: in case a forbidden name (containing two underscores)
134                       is found in ``code_obj``
135
136    .. note:: actually forbids every name containing 2 underscores
137    """
138    for name in code_obj.co_names:
139        if "__" in name or name in _UNSAFE_ATTRIBUTES:
140            raise NameError('Access to forbidden name %r (%r)' % (name, expr))
141
142def assert_valid_codeobj(allowed_codes, code_obj, expr):
143    """ Asserts that the provided code object validates against the bytecode
144    and name constraints.
145
146    Recursively validates the code objects stored in its co_consts in case
147    lambdas are being created/used (lambdas generate their own separated code
148    objects and don't live in the root one)
149
150    :param allowed_codes: list of permissible bytecode instructions
151    :type allowed_codes: set(int)
152    :param code_obj: code object to name-validate
153    :type code_obj: CodeType
154    :param str expr: expression corresponding to the code object, for debugging
155                     purposes
156    :raises ValueError: in case of forbidden bytecode in ``code_obj``
157    :raises NameError: in case a forbidden name (containing two underscores)
158                       is found in ``code_obj``
159    """
160    assert_no_dunder_name(code_obj, expr)
161
162    # set operations are almost twice as fast as a manual iteration + condition
163    # when loading /web according to line_profiler
164    code_codes = {i.opcode for i in dis.get_instructions(code_obj)}
165    if not allowed_codes >= code_codes:
166        raise ValueError("forbidden opcode(s) in %r: %s" % (expr, ', '.join(opname[x] for x in (code_codes - allowed_codes))))
167
168    for const in code_obj.co_consts:
169        if isinstance(const, CodeType):
170            assert_valid_codeobj(allowed_codes, const, 'lambda')
171
172def test_expr(expr, allowed_codes, mode="eval"):
173    """test_expr(expression, allowed_codes[, mode]) -> code_object
174
175    Test that the expression contains only the allowed opcodes.
176    If the expression is valid and contains only allowed codes,
177    return the compiled code object.
178    Otherwise raise a ValueError, a Syntax Error or TypeError accordingly.
179    """
180    try:
181        if mode == 'eval':
182            # eval() does not like leading/trailing whitespace
183            expr = expr.strip()
184        code_obj = compile(expr, "", mode)
185    except (SyntaxError, TypeError, ValueError):
186        raise
187    except Exception as e:
188        raise ValueError('"%s" while compiling\n%r' % (ustr(e), expr))
189    assert_valid_codeobj(allowed_codes, code_obj, expr)
190    return code_obj
191
192
193def const_eval(expr):
194    """const_eval(expression) -> value
195
196    Safe Python constant evaluation
197
198    Evaluates a string that contains an expression describing
199    a Python constant. Strings that are not valid Python expressions
200    or that contain other code besides the constant raise ValueError.
201
202    >>> const_eval("10")
203    10
204    >>> const_eval("[1,2, (3,4), {'foo':'bar'}]")
205    [1, 2, (3, 4), {'foo': 'bar'}]
206    >>> const_eval("1+2")
207    Traceback (most recent call last):
208    ...
209    ValueError: opcode BINARY_ADD not allowed
210    """
211    c = test_expr(expr, _CONST_OPCODES)
212    return unsafe_eval(c)
213
214def expr_eval(expr):
215    """expr_eval(expression) -> value
216
217    Restricted Python expression evaluation
218
219    Evaluates a string that contains an expression that only
220    uses Python constants. This can be used to e.g. evaluate
221    a numerical expression from an untrusted source.
222
223    >>> expr_eval("1+2")
224    3
225    >>> expr_eval("[1,2]*2")
226    [1, 2, 1, 2]
227    >>> expr_eval("__import__('sys').modules")
228    Traceback (most recent call last):
229    ...
230    ValueError: opcode LOAD_NAME not allowed
231    """
232    c = test_expr(expr, _EXPR_OPCODES)
233    return unsafe_eval(c)
234
235def _import(name, globals=None, locals=None, fromlist=None, level=-1):
236    if globals is None:
237        globals = {}
238    if locals is None:
239        locals = {}
240    if fromlist is None:
241        fromlist = []
242    if name in _ALLOWED_MODULES:
243        return __import__(name, globals, locals, level)
244    raise ImportError(name)
245_BUILTINS = {
246    '__import__': _import,
247    'True': True,
248    'False': False,
249    'None': None,
250    'bytes': bytes,
251    'str': str,
252    'unicode': str,
253    'bool': bool,
254    'int': int,
255    'float': float,
256    'enumerate': enumerate,
257    'dict': dict,
258    'list': list,
259    'tuple': tuple,
260    'map': map,
261    'abs': abs,
262    'min': min,
263    'max': max,
264    'sum': sum,
265    'reduce': functools.reduce,
266    'filter': filter,
267    'sorted': sorted,
268    'round': round,
269    'len': len,
270    'repr': repr,
271    'set': set,
272    'all': all,
273    'any': any,
274    'ord': ord,
275    'chr': chr,
276    'divmod': divmod,
277    'isinstance': isinstance,
278    'range': range,
279    'xrange': range,
280    'zip': zip,
281    'Exception': Exception,
282}
283def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False, locals_builtins=False):
284    """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
285
286    System-restricted Python expression evaluation
287
288    Evaluates a string that contains an expression that mostly
289    uses Python constants, arithmetic expressions and the
290    objects directly provided in context.
291
292    This can be used to e.g. evaluate
293    an OpenERP domain expression from an untrusted source.
294
295    :throws TypeError: If the expression provided is a code object
296    :throws SyntaxError: If the expression provided is not valid Python
297    :throws NameError: If the expression provided accesses forbidden names
298    :throws ValueError: If the expression provided uses forbidden bytecode
299    """
300    if type(expr) is CodeType:
301        raise TypeError("safe_eval does not allow direct evaluation of code objects.")
302
303    # prevent altering the globals/locals from within the sandbox
304    # by taking a copy.
305    if not nocopy:
306        # isinstance() does not work below, we want *exactly* the dict class
307        if (globals_dict is not None and type(globals_dict) is not dict) \
308                or (locals_dict is not None and type(locals_dict) is not dict):
309            _logger.warning(
310                "Looks like you are trying to pass a dynamic environment, "
311                "you should probably pass nocopy=True to safe_eval().")
312        if globals_dict is not None:
313            globals_dict = dict(globals_dict)
314        if locals_dict is not None:
315            locals_dict = dict(locals_dict)
316
317    check_values(globals_dict)
318    check_values(locals_dict)
319
320    if globals_dict is None:
321        globals_dict = {}
322
323    globals_dict['__builtins__'] = _BUILTINS
324    if locals_builtins:
325        if locals_dict is None:
326            locals_dict = {}
327        locals_dict.update(_BUILTINS)
328    c = test_expr(expr, _SAFE_OPCODES, mode=mode)
329    try:
330        return unsafe_eval(c, globals_dict, locals_dict)
331    except odoo.exceptions.UserError:
332        raise
333    except odoo.exceptions.RedirectWarning:
334        raise
335    except werkzeug.exceptions.HTTPException:
336        raise
337    except odoo.http.AuthenticationError:
338        raise
339    except OperationalError:
340        # Do not hide PostgreSQL low-level exceptions, to let the auto-replay
341        # of serialized transactions work its magic
342        raise
343    except ZeroDivisionError:
344        raise
345    except Exception as e:
346        raise ValueError('%s: "%s" while evaluating\n%r' % (ustr(type(e)), ustr(e), expr))
347def test_python_expr(expr, mode="eval"):
348    try:
349        test_expr(expr, _SAFE_OPCODES, mode=mode)
350    except (SyntaxError, TypeError, ValueError) as err:
351        if len(err.args) >= 2 and len(err.args[1]) >= 4:
352            error = {
353                'message': err.args[0],
354                'filename': err.args[1][0],
355                'lineno': err.args[1][1],
356                'offset': err.args[1][2],
357                'error_line': err.args[1][3],
358            }
359            msg = "%s : %s at line %d\n%s" % (type(err).__name__, error['message'], error['lineno'], error['error_line'])
360        else:
361            msg = ustr(err)
362        return msg
363    return False
364
365
366def check_values(d):
367    if not d:
368        return d
369    for v in d.values():
370        if isinstance(v, types.ModuleType):
371            raise TypeError(f"""Module {v} can not be used in evaluation contexts
372
373Prefer providing only the items necessary for your intended use.
374
375If a "module" is necessary for backwards compatibility, use
376`odoo.tools.safe_eval.wrap_module` to generate a wrapper recursively
377whitelisting allowed attributes.
378
379Pre-wrapped modules are provided as attributes of `odoo.tools.safe_eval`.
380""")
381    return d
382
383class wrap_module:
384    def __init__(self, module, attributes):
385        """Helper for wrapping a package/module to expose selected attributes
386
387        :param module: the actual package/module to wrap, as returned by ``import <module>``
388        :param iterable attributes: attributes to expose / whitelist. If a dict,
389                                    the keys are the attributes and the values
390                                    are used as an ``attributes`` in case the
391                                    corresponding item is a submodule
392        """
393        # builtin modules don't have a __file__ at all
394        modfile = getattr(module, '__file__', '(built-in)')
395        self._repr = f"<wrapped {module.__name__!r} ({modfile})>"
396        for attrib in attributes:
397            target = getattr(module, attrib)
398            if isinstance(target, types.ModuleType):
399                target = wrap_module(target, attributes[attrib])
400            setattr(self, attrib, target)
401
402    def __repr__(self):
403        return self._repr
404
405# dateutil submodules are lazy so need to import them for them to "exist"
406import dateutil
407mods = ['parser', 'relativedelta', 'rrule', 'tz']
408for mod in mods:
409    __import__('dateutil.%s' % mod)
410datetime = wrap_module(__import__('datetime'), ['date', 'datetime', 'time', 'timedelta', 'timezone', 'tzinfo', 'MAXYEAR', 'MINYEAR'])
411dateutil = wrap_module(dateutil, {
412    mod: getattr(dateutil, mod).__all__
413    for mod in mods
414})
415json = wrap_module(__import__('json'), ['loads', 'dumps'])
416time = wrap_module(__import__('time'), ['time', 'strptime', 'strftime'])
417pytz = wrap_module(__import__('pytz'), [
418    'utc', 'UTC', 'timezone',
419])
420