diff --git a/bin/tools/safe_eval.py b/bin/tools/safe_eval.py index a7fce27d2e0..5eeb6258dc0 100644 --- a/bin/tools/safe_eval.py +++ b/bin/tools/safe_eval.py @@ -1,66 +1,241 @@ # -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2004-2010 OpenERP s.a. (). # -# Copyright P. Christeas 2008,2009 +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. # +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. # -# WARNING: This program as such is intended to be used by professional -# programmers who take the whole responsability of assessing all potential -# consequences resulting from its eventual inadequacies and bugs -# End users who are looking for a ready-to-use solution with commercial -# garantees and support are strongly adviced to contract a Free Software -# Service Company +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . # -# This program is Free Software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -############################################################################### +############################################################################## -__export_bis = {} -import sys +""" +safe_eval module - methods intended to provide more restricted alternatives to + evaluate simple and/or untrusted code. + +Methods in this module are typically used as alternatives to eval() to parse +OpenERP domain strings, conditions and expressions, mostly based on locals +condition/math builtins. +""" -def __init_ebis(): - global __export_bis +# Module partially ripped from/inspired by several different sources: +# - http://code.activestate.com/recipes/286134/ +# - safe_eval in lp:~xrg/openobject-server/optimize-5.0 +# - safe_eval in tryton http://hg.tryton.org/hgwebdir.cgi/trytond/rev/bbb5f73319ad +# - python 2.6's ast.literal_eval - _evars = [ 'abs', 'all', 'any', 'basestring' , 'bool', - 'chr', 'cmp','complex', 'dict', 'divmod', 'enumerate', - 'float', 'frozenset', 'getattr', 'hasattr', 'hash', - 'hex', 'id','int', 'iter', 'len', 'list', 'long', 'map', 'max', - 'min', 'oct', 'ord','pow', 'range', 'reduce', 'repr', - 'reversed', 'round', 'set', 'setattr', 'slice','sorted', 'str', - 'sum', 'tuple','type', 'unichr','unicode', 'xrange', - 'True','False', 'None', 'NotImplemented', 'Ellipsis', ] +from opcode import HAVE_ARGUMENT, opmap, opname - if sys.version_info[0:2] >= (2,6): - _evars.extend(['bin', 'format', 'next']) - for v in _evars: - __export_bis[v] = __builtins__[v] +_CONST_CODES = set(opmap[x] for x in [ + 'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP', + 'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE', + 'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap) + +_EXPR_CODES = _CONST_CODES.union(set(opmap[x] for x in [ + 'UNARY_POSITIVE', 'UNARY_NEGATIVE', 'UNARY_NOT', + 'UNARY_INVERT', 'BINARY_POWER', 'BINARY_MULTIPLY', + 'BINARY_DIVIDE', 'BINARY_FLOOR_DIVIDE', 'BINARY_TRUE_DIVIDE', + 'BINARY_MODULO', 'BINARY_ADD', 'BINARY_SUBTRACT', 'BINARY_SUBSCR', + 'BINARY_LSHIFT', 'BINARY_RSHIFT', 'BINARY_AND', 'BINARY_XOR', + 'BINARY_OR'] if x in opmap)) + +_SAFE_CODES = _EXPR_CODES.union(set(opmap[x] for x in [ + 'STORE_MAP', 'LOAD_NAME', 'CALL_FUNCTION', 'COMPARE_OP', 'LOAD_ATTR', + 'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'JUMP_ABSOLUTE', + 'DELETE_NAME', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', + ] if x in opmap)) -__init_ebis() +def _get_opcodes(codeobj): + """_get_opcodes(codeobj) -> [opcodes] + + Extract the actual opcodes as a list from a code object + + >>> c = compile("[1 + 2, (1,2)]", "", "eval") + >>> _get_opcodes(c) + [100, 100, 23, 100, 100, 102, 103, 83] + """ + i = 0 + opcodes = [] + s = codeobj.co_code + while i < len(s): + code = ord(s[i]) + opcodes.append(code) + if code >= HAVE_ARGUMENT: + i += 3 + else: + i += 1 + return opcodes + +def test_expr(expr, allowed_codes): + """test_expr(expression) -> code_object + + Test that the expression contains only the allowed opcodes. + If the expression is valid and contains only allowed codes, + return the compiled code object. Otherwise raise a ValueError. + """ + try: + code_obj = compile(expr, "", "eval") + except: + raise ValueError("%s is not a valid expression" % expr) + for code in _get_opcodes(code_obj): + if code not in allowed_codes: + raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr)) + return code_obj -def safe_eval(expr,sglobals,slocals = None): - """ A little safer version of eval(). - This one, will use fewer builtin functions, so that only - arithmetic and logic expressions can really work """ +def const_eval(expr): + """const_eval(expression) -> value - global __export_bis + Safe Python constant evaluation + + Evaluates a string that contains an expression describing + a Python constant. Strings that are not valid Python expressions + or that contain other code besides the constant raise ValueError. + + >>> const_eval("10") + 10 + >>> const_eval("[1,2, (3,4), {'foo':'bar'}]") + [1, 2, (3, 4), {'foo': 'bar'}] + >>> const_eval("1+2") + Traceback (most recent call last): + ... + ValueError: opcode BINARY_ADD not allowed + """ + c = test_expr(expr, _CONST_CODES) + return eval(c) + +def expr_eval(expr): + """expr_eval(expression) -> value + + Restricted Python expression evaluation + + Evaluates a string that contains an expression that only + uses Python constants. This can be used to e.g. evaluate + a numerical expression from an untrusted source. + + >>> expr_eval("1+2") + 3 + >>> expr_eval("[1,2]*2") + [1, 2, 1, 2] + >>> expr_eval("__import__('sys').modules") + Traceback (most recent call last): + ... + ValueError: opcode LOAD_NAME not allowed + """ + c = test_expr(expr, _EXPR_CODES) + return eval(c) + + +# Port of Python 2.6's ast.literal_eval for use under Python 2.5 +SAFE_CONSTANTS = {'None': None, 'True': True, 'False': False} + +try: + # first, try importing directly + from ast import literal_eval +except ImportError: + from _ast import * + + def _convert(node): + if isinstance(node, Str): + return node.s + elif isinstance(node, Num): + return node.n + elif isinstance(node, Tuple): + return tuple(map(_convert, node.elts)) + elif isinstance(node, List): + return list(map(_convert, node.elts)) + elif isinstance(node, Dict): + return dict((_convert(k), _convert(v)) for k, v + in zip(node.keys, node.values)) + elif isinstance(node, Name): + if node.id in SAFE_CONSTANTS: + return SAFE_CONSTANTS[node.id] + raise ValueError('malformed or disallowed expression') + + def parse(expr, filename='', mode='eval'): + """parse(source[, filename], mode]] -> code object + Parse an expression into an AST node. + Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST). + """ + return compile(expr, filename, mode, PyCF_ONLY_AST) + + def literal_eval(node_or_string): + """literal_eval(expression) -> value + Safely evaluate an expression node or a string containing a Python + expression. The string or node provided may only consist of the + following Python literal structures: strings, numbers, tuples, + lists, dicts, booleans, and None. + + >>> literal_eval('[1,True,"spam"]') + [1, True, 'spam'] + + >>> literal_eval('1+3') + Traceback (most recent call last): + ... + ValueError: malformed or disallowed expression + """ + if isinstance(node_or_string, basestring): + node_or_string = parse(node_or_string) + if isinstance(node_or_string, Expression): + node_or_string = node_or_string.body + return _convert(node_or_string) + + + +def safe_eval(expr, context = None): + """safe_eval(expression, context) -> value + + System-restricted Python expression evaluation + + Evaluates a string that contains an expression that mostly + uses Python constants, arithmetic expressions and the + use of the objects provided in context. + + This can be used to e.g. evaluate + an OpenERP domain expression from an untrusted expr. + + >>> safe_eval("__import__('sys').modules") + Traceback (most recent call last): + ... + ValueError: opcode LOAD_NAME not allowed + + """ + if '__subclasses__' in expr: + raise ValueError('expression not allowed (__subclasses__)') + code_obj = compile(expr, '', 'eval') + byte_codes = code_obj.co_code + i = 0 + while i < len(byte_codes): + op_code = ord(byte_codes[i]) + if op_code not in _SAFE_CODES: + raise ValueError('opcode %byte_codes not allowed' % dis.opname[op_code]) + if op_code >= HAVE_ARGUMENT: + i += 3 + else: + i += 1 + return eval(code_obj, {'__builtins__': { + 'True': True, + 'False': False, + 'None': None, + 'str': str, + 'globals': locals, + 'locals': locals, + 'bool': bool, + 'dict': dict, + 'list': list, + 'tuple': tuple, + } + }, context) - if not sglobals.has_key('__builtins__'): - # we copy, because we wouldn't want successive calls to safe_eval - # to be able to alter the builtins. - sglobals['__builtins__'] = __export_bis.copy() - return eval(expr,sglobals,slocals) -#eof