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