From a7a2241bca6839d87b4fa45706704637b7d00f71 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 9 Oct 2013 14:40:32 +0200 Subject: [PATCH] [IMP] tools.safe_eval_qweb: methods intended to provide more restricted alternatives to evaluate simple and/or untrusted code, objects and browse record. 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 and browse record values. (use jinja sandboxed environment) This is done on purpose: it prevents incidental or malicious execution of Python code that may break the security of the server. bzr revid: chm@openerp.com-20131009124032-elygz03eg23uq1yp --- openerp/tools/qweb.py | 49 +++----------- openerp/tools/safe_eval_qweb.py | 112 ++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 40 deletions(-) create mode 100644 openerp/tools/safe_eval_qweb.py diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index a448cb0f1a6..abe60fbddef 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -2,7 +2,7 @@ import logging import re import werkzeug.utils -#from openerp.tools.safe_eval import safe_eval as eval +from openerp.tools.safe_eval_qweb import safe_eval_qweb as eval, UndefinedError, SecurityError import xml # FIXME use lxml import traceback @@ -10,42 +10,6 @@ from openerp.osv import osv, orm _logger = logging.getLogger(__name__) -BUILTINS = { - 'False': False, - 'None': None, - 'True': True, - 'abs': abs, - 'bool': bool, - 'dict': dict, - 'filter': filter, - 'len': len, - 'list': list, - 'map': map, - 'max': max, - 'min': min, - 'reduce': reduce, - 'repr': repr, - 'round': round, - 'set': set, - 'str': str, - 'tuple': tuple, -} - -class QWebContext(dict): - def __init__(self, data, undefined_handler=None): - self.undefined_handler = undefined_handler - dic = BUILTINS.copy() - dic.update(data) - super(QWebContext, self).__init__(dic) - self['defined'] = lambda key: key in self - - def __getitem__(self, key): - if key in self: - return self.get(key) - elif not self.undefined_handler: - raise NameError("QWeb: name %r is not defined while rendering template %r" % (key, self.get('__template__'))) - else: - return self.get(key, self.undefined_handler(key, self)) class QWebXml(object): """QWeb Xml templating engine @@ -112,6 +76,11 @@ class QWebXml(object): return eval(expr, None, v) except (osv.except_osv, orm.except_orm), err: raise orm.except_orm("QWeb Error", "Invalid expression %r while rendering template '%s'.\n\n%s" % (expr, v.get('__template__'), err[1])) + except (UndefinedError, SecurityError), err: + if self.undefined_handler: + return self.undefined_handler(expr, v) + else: + raise SyntaxError(err.message) except Exception: raise SyntaxError("QWeb: invalid expression %r while rendering template '%s'.\n\n%s" % (expr, v.get('__template__'), traceback.format_exc())) @@ -122,6 +91,7 @@ class QWebXml(object): if expr == "0": return v.get(0, '') val = self.eval(expr, v) + if isinstance(val, unicode): return val.encode("utf8") return str(val) @@ -156,7 +126,6 @@ class QWebXml(object): v['__caller__'] = stack[-1] stack.append(tname) v['__stack__'] = stack - v = QWebContext(v, self.undefined_handler) return self.render_node(self.get_template(tname), v) def render_node(self, e, v): @@ -275,7 +244,7 @@ class QWebXml(object): enum = self.eval_object(expr, v) if enum is not None: var = t_att.get('as', expr).replace('.', '_') - d = QWebContext(v.copy(), self.undefined_handler) + d = v.copy() size = -1 if isinstance(enum, (list, tuple)): size = len(enum) @@ -316,7 +285,7 @@ class QWebXml(object): if "import" in t_att: d = v else: - d = QWebContext(v.copy(), self.undefined_handler) + d = v.copy() d[0] = self.render_element(e, t_att, g_att, d) return self.render(self.eval_format(t_att["call"], d), d) diff --git a/openerp/tools/safe_eval_qweb.py b/openerp/tools/safe_eval_qweb.py new file mode 100644 index 00000000000..0e81228c840 --- /dev/null +++ b/openerp/tools/safe_eval_qweb.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +############################################################################## +# Copyright (C) 2004-2012 OpenERP s.a. (). +# +# 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. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" +safe_eval_qweb - methods intended to provide more restricted alternatives + to evaluate simple and/or untrusted code, objects and browse record. + +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 and browse record values. + +Only safe Python attributes of objects may be accessed. + +Unsafe / not accepted attributes: +* All "private" attributes (not starting with '_') +* browse +* search +* read +* unlink +* read_group + +This is done on purpose: it prevents incidental or malicious execution of +Python code that may break the security of the server. + +""" + +from urllib import urlencode, quote as quote +import datetime +import dateutil.relativedelta as relativedelta +import logging + +# We use a jinja2 sandboxed environment to render qWeb templates. +from jinja2.sandbox import SandboxedEnvironment +from jinja2.exceptions import SecurityError, UndefinedError + + +_logger = logging.getLogger(__name__) + + +BUILTINS = { + 'False': False, + 'None': None, + 'True': True, + 'abs': abs, + 'bool': bool, + 'dict': dict, + 'filter': filter, + 'len': len, + 'list': list, + 'map': map, + 'max': max, + 'min': min, + 'reduce': reduce, + 'repr': repr, + 'round': round, + 'set': set, + 'str': str, + 'tuple': tuple, + 'str': str, + 'quote': quote, + 'urlencode': urlencode, + 'datetime': datetime, + # dateutil.relativedelta is an old-style class and cannot be directly + # instanciated wihtin a jinja2 expression, so a lambda "proxy" is + # is needed, apparently. + 'relativedelta': lambda *a, **kw : relativedelta.relativedelta(*a, **kw), +} +UNSAFE = [str("browse"), str("search"), str("read"), str("unlink"), str("read_group")] + + +class qWebSandboxedEnvironment(SandboxedEnvironment): + def is_safe_attribute(self, obj, attr, value): + res = super(qWebSandboxedEnvironment, self).is_safe_attribute(obj, attr, value) + if str(attr) in UNSAFE or not res: + raise SecurityError("access to attribute '%s' of '%s' object is unsafe." % (attr,obj)) + return res + +def safe_eval_qweb(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False): + if globals_dict is None: + globals_dict = {} + if locals_dict is None: + locals_dict = {} + + if not isinstance(locals_dict, dict): + _logger.warning("The globals_dict and locals_dict of the dynamic environment "+ + "pass to safe_eval_qweb must be dict") + + context = dict(globals_dict) + context = dict(locals_dict) + context.update(BUILTINS) + + env = qWebSandboxedEnvironment(variable_start_string="${", variable_end_string="}") + env.globals.update(context) + + # use jinja environment + return env.compile_expression(expr)()