Launchpad automatic translations update.
[odoo/odoo.git] / bin / tools / safe_eval.py
index a7fce27..6c068d8 100644 (file)
 # -*- coding: utf-8 -*-
+##############################################################################
+#    Copyright (C) 2004-2010 OpenERP s.a. (<http://www.openerp.com>).
 #
-# Copyright P. Christeas <p_christ@hol.gr> 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 <http://www.gnu.org/licenses/>.
 #
-# 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.
-###############################################################################
+##############################################################################
+
+"""
+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.
+"""
+
+# 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
+
+from opcode import HAVE_ARGUMENT, opmap, opname
+from types import CodeType
+import logging
+import os
+
+__all__ = ['test_expr', 'literal_eval', 'safe_eval', 'const_eval', 'ext_eval' ]
+
+# The time module is usually already provided in the safe_eval environment
+# but some code, e.g. datetime.datetime.now() (Windows/Python 2.5.2, bug
+# lp:703841), does import time.
+_ALLOWED_MODULES = ['_strptime', 'time']
+
+_CONST_OPCODES = set(opmap[x] for x in [
+    'POP_TOP', 'ROT_TWO', 'ROT_THREE', 'ROT_FOUR', 'DUP_TOP','POP_BLOCK','SETUP_LOOP',
+    'BUILD_LIST', 'BUILD_MAP', 'BUILD_TUPLE',
+    'LOAD_CONST', 'RETURN_VALUE', 'STORE_SUBSCR'] if x in opmap)
+
+_EXPR_OPCODES = _CONST_OPCODES.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_OPCODES = _EXPR_OPCODES.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', 'DELETE_NAME',
+    'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
+    'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3',
+    # New in Python 2.7 - http://bugs.python.org/issue4715 :
+    'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
+    'POP_JUMP_IF_TRUE'
+    ] if x in opmap))
+
+_logger = logging.getLogger('safe_eval')
+
+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 = []
+    byte_codes = codeobj.co_code
+    while i < len(byte_codes):
+        code = ord(byte_codes[i])
+        opcodes.append(code)
+        if code >= HAVE_ARGUMENT:
+            i += 3
+        else:
+            i += 1
+    return opcodes
+
+def test_expr(expr, allowed_codes, mode="eval"):
+    """test_expr(expression, allowed_codes[, mode]) -> 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, a Syntax Error or TypeError accordingly.
+    """
+    try:
+        if mode == 'eval':
+            # eval() does not like leading/trailing whitespace
+            expr = expr.strip()
+        code_obj = compile(expr, "", mode)
+    except (SyntaxError, TypeError):
+        _logger.debug('Invalid eval expression', exc_info=True)
+        raise
+    except Exception:
+        _logger.debug('Disallowed or invalid eval expression', exc_info=True)
+        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 const_eval(expr):
+    """const_eval(expression) -> value
+
+    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_OPCODES)
+    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_OPCODES)
+    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:
+    import _ast as ast
+
+    def _convert(node):
+        if isinstance(node, ast.Str):
+            return node.s
+        elif isinstance(node, ast.Num):
+            return node.n
+        elif isinstance(node, ast.Tuple):
+            return tuple(map(_convert, node.elts))
+        elif isinstance(node, ast.List):
+            return list(map(_convert, node.elts))
+        elif isinstance(node, ast.Dict):
+            return dict((_convert(k), _convert(v)) for k, v
+                        in zip(node.keys, node.values))
+        elif isinstance(node, ast.Name):
+            if node.id in SAFE_CONSTANTS:
+                return SAFE_CONSTANTS[node.id]
+        raise ValueError('malformed or disallowed expression')
+
+    def parse(expr, filename='<unknown>', 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, ast.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, ast.Expression):
+            node_or_string = node_or_string.body
+        return _convert(node_or_string)
+
+def _import(name, globals={}, locals={}, fromlist=[], level=-1):
+    if name in _ALLOWED_MODULES:
+        return __import__(name, globals, locals, level)
+    raise ImportError(name)
+
+def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
+    """safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
 
-__export_bis = {}
-import sys
+    System-restricted Python expression evaluation
 
-def __init_ebis():
-    global __export_bis
+    Evaluates a string that contains an expression that mostly
+    uses Python constants, arithmetic expressions and the
+    objects directly provided in context.
 
-    _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', ]
+    This can be used to e.g. evaluate
+    an OpenERP domain expression from an untrusted source.
 
-    if sys.version_info[0:2] >= (2,6):
-        _evars.extend(['bin', 'format', 'next'])
-    for v in _evars:
-        __export_bis[v] = __builtins__[v]
+    Throws TypeError, SyntaxError or ValueError (not allowed) accordingly.
 
+    >>> safe_eval("__import__('sys').modules")
+    Traceback (most recent call last):
+    ...
+    ValueError: opcode LOAD_NAME not allowed
 
-__init_ebis()
+    """
+    if isinstance(expr, CodeType):
+        raise ValueError("safe_eval does not allow direct evaluation of code objects.")
 
+    if '__subclasses__' in expr:
+       raise ValueError('expression not allowed (__subclasses__)')
 
-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 """
+    if globals_dict is None:
+        globals_dict = {}
 
-    global __export_bis
+    # prevent altering the globals/locals from within the sandbox
+    # by taking a copy.
+    if not nocopy:
+        # isinstance() does not work below, we want *exactly* the dict class
+        if (globals_dict is not None and type(globals_dict) is not dict) \
+            or (locals_dict is not None and type(locals_dict) is not dict):
+            logging.getLogger('safe_eval').warning('Looks like you are trying to pass a dynamic environment,"\
+                              "you should probably pass nocopy=True to safe_eval()')
 
-    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()
+        globals_dict = dict(globals_dict)
+        if locals_dict is not None:
+            locals_dict = dict(locals_dict)
 
-    return eval(expr,sglobals,slocals)
+    globals_dict.update(
+            __builtins__ = {
+                '__import__': _import,
+                'True': True,
+                'False': False,
+                'None': None,
+                'str': str,
+                'globals': locals,
+                'locals': locals,
+                'bool': bool,
+                'dict': dict,
+                'list': list,
+                'tuple': tuple,
+                'map': map,
+                'abs': abs,
+                'reduce': reduce,
+                'filter': filter,
+                'round': round,
+                'len': len,
+                'set' : set
+            }
+    )
+    return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
 
-#eof
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: