[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
This commit is contained in:
Christophe Matthieu 2013-10-09 14:40:32 +02:00
parent 00532ab64e
commit a7a2241bca
2 changed files with 121 additions and 40 deletions

View File

@ -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)

View File

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
##############################################################################
# Copyright (C) 2004-2012 OpenERP s.a. (<http://www.openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
"""
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)()