bzr revid: fp@tinyerp.com-20100604161814-2vdkl7im7ajnd6gl
This commit is contained in:
Fabien Pinckaers
2010-06-04 18:18:14 +02:00
28 changed files with 402 additions and 110 deletions
+1
View File
@@ -28,6 +28,7 @@ import zipimport
import osv
import tools
import tools.osutil
from tools.safe_eval import safe_eval as eval
import pooler
+1
View File
@@ -75,6 +75,7 @@
],
'test': [
'test/base_test.xml',
#'test/base_test.yml'
'test/test_context.xml',
'bug_lp541545.xml',
],
+3 -2
View File
@@ -78,7 +78,8 @@
<form string="Users">
<notebook colspan="4">
<page string="Current Activity">
<field name="company_id" widget="selection" readonly="0" context="{'user_preference':True}"/>
<field name="company_id" widget="selection" readonly="0"
context="{'user_id': self, 'user_preference': 1}"/>
<newline/>
<separator colspan="4" string="Preferences"/>
</page>
@@ -110,7 +111,7 @@
<page string="User">
<field name="address_id" select="1"/>
<field name="user_email" widget="email"/>
<field name="company_id" required="1"/>
<field name="company_id" required="1" context="{'user_id': self, 'user_preference': 1}"/>
<field name="action_id" required="True"/>
<field domain="[('usage','=','menu')]" name="menu_id" required="True"/>
<field name="context_lang"/>
+2 -2
View File
@@ -7,13 +7,13 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.4\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2009-12-18 08:39+0000\n"
"PO-Revision-Date: 2010-06-02 04:11+0000\n"
"PO-Revision-Date: 2010-06-03 03:58+0000\n"
"Last-Translator: goranc <goranc@gmail.com>\n"
"Language-Team: openerp-translators\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2010-06-03 03:33+0000\n"
"X-Launchpad-Export-Date: 2010-06-04 03:44+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
"Language: hr\n"
+2 -2
View File
@@ -8,13 +8,13 @@ msgstr ""
"Project-Id-Version: openobject-server\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2009-12-18 08:39+0000\n"
"PO-Revision-Date: 2010-06-02 04:08+0000\n"
"PO-Revision-Date: 2010-06-03 03:56+0000\n"
"Last-Translator: Harry (Open ERP) <hmo@tinyerp.com>\n"
"Language-Team: Japanese <ja@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2010-06-03 03:33+0000\n"
"X-Launchpad-Export-Date: 2010-06-04 03:44+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. module: base
+2 -2
View File
@@ -7,13 +7,13 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.0\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2009-12-18 08:39+0000\n"
"PO-Revision-Date: 2010-06-02 04:18+0000\n"
"PO-Revision-Date: 2010-06-03 03:59+0000\n"
"Last-Translator: Anders Wallenquist <anders.wallenquist@vertel.se>\n"
"Language-Team: <>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2010-06-03 03:33+0000\n"
"X-Launchpad-Export-Date: 2010-06-04 03:44+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. module: base
+2 -2
View File
@@ -7,13 +7,13 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.0\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2009-12-18 08:39+0000\n"
"PO-Revision-Date: 2010-06-02 04:06+0000\n"
"PO-Revision-Date: 2010-06-03 03:59+0000\n"
"Last-Translator: Fabien (Open ERP) <fp@tinyerp.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2010-06-03 03:33+0000\n"
"X-Launchpad-Export-Date: 2010-06-04 03:45+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. module: base
+2 -2
View File
@@ -7,13 +7,13 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.4\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2009-12-18 08:39+0000\n"
"PO-Revision-Date: 2010-06-02 04:06+0000\n"
"PO-Revision-Date: 2010-06-03 03:59+0000\n"
"Last-Translator: OpenERP Administrators <Unknown>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2010-06-03 03:33+0000\n"
"X-Launchpad-Export-Date: 2010-06-04 03:45+0000\n"
"X-Generator: Launchpad (build Unknown)\n"
#. module: base
+2 -1
View File
@@ -20,6 +20,7 @@
##############################################################################
from osv import fields,osv
from tools.safe_eval import safe_eval as eval
import tools
import time
from tools.config import config
@@ -564,7 +565,7 @@ class actions_server(osv.osv):
'object':obj,
'obj': obj,
}
exec action.code in localdict
eval(action.code, localdict, "exec")
if 'action' in localdict:
return localdict['action']
else:
+1
View File
@@ -25,6 +25,7 @@ from datetime import datetime
from dateutil.relativedelta import relativedelta
import netsvc
import tools
from tools.safe_eval import safe_eval as eval
import pooler
from osv import fields, osv
+1
View File
@@ -24,6 +24,7 @@ from osv.orm import browse_null
import ir
import report.custom
from tools.translate import _
from tools.safe_eval import safe_eval as eval
import netsvc
class report_custom(osv.osv):
+1
View File
@@ -22,6 +22,7 @@
from osv import fields,osv
import time
import tools
from tools.safe_eval import safe_eval as eval
class ir_rule(osv.osv):
_name = 'ir.rule'
+1
View File
@@ -22,6 +22,7 @@
from osv import fields,osv
from lxml import etree
from tools import graph
from tools.safe_eval import safe_eval as eval
import tools
import netsvc
import os
+11 -3
View File
@@ -24,6 +24,7 @@ from osv import fields
import os
import tools
from tools.translate import _
from tools.safe_eval import safe_eval as eval
class multi_company_default(osv.osv):
"""
@@ -91,11 +92,18 @@ class res_company(osv.osv):
def search(self, cr, uid, args, offset=0, limit=None, order=None,
context=None, count=False):
user_preference = context and context.get('user_preference', False) or False
if context is None:
context = {}
user_preference = context.get('user_preference', False)
if user_preference:
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
cmp_ids = list(set([user.company_id.id] + [cmp.id for cmp in user.company_ids]))
# TODO: improve this as soon as the client sends the proper
# combination of active_id and active_model we'll be able to
# use active_id here to restrict to the user being modified instead
# of current user.
user_id = context.get('user_id', uid)
user = self.pool.get('res.users').browse(cr, uid, user_id, context=context)
cmp_ids = list(set([user.company_id.id] + [cmp.id for cmp in user.company_ids]))
return cmp_ids
return super(res_company, self).search(cr, uid, args, offset=offset, limit=limit, order=order,
context=context, count=count)
+1
View File
@@ -22,6 +22,7 @@
from osv import fields, osv
from locale import localeconv
import tools
from tools.safe_eval import safe_eval as eval
class lang(osv.osv):
_name = "res.lang"
+15 -11
View File
@@ -233,8 +233,13 @@ class users(osv.osv):
'menu_id': fields.many2one('ir.actions.actions', 'Menu Action'),
'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'),
'roles_id': fields.many2many('res.roles', 'res_roles_users_rel', 'uid', 'rid', 'Roles'),
# Special behavior for this field: res.company.search() will only return the companies
# available to the current user (should be the user's companies?), when the user_preference
# context is set.
'company_id': fields.many2one('res.company', 'Company', required=True,
help="The company this user is currently working for."),
help="The company this user is currently working for.", context={'user_preference': True}),
'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'),
'context_lang': fields.selection(_lang_get, 'Language', required=True,
help="Sets the language for the user's user interface, when UI "
@@ -277,7 +282,7 @@ class users(osv.osv):
return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context))
_constraints = [
(_check_company, 'The chosen company is not in the allowed companies', ['company_id', 'company_ids']),
(_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']),
]
_sql_constraints = [
@@ -327,19 +332,18 @@ class users(osv.osv):
'groups_id': _get_group,
'address_id': False,
}
def company_get(self, cr, uid, uid2, context={}):
def company_get(self, cr, uid, uid2, context=None):
return self._get_company(cr, uid, context=context, uid2=uid2)
company_get = tools.cache()(company_get)
def write(self, cr, uid, ids, values, *args, **argv):
if (ids == [uid]):
ok = True
for k in values.keys():
if k not in ('password','signature','action_id', 'context_lang', 'context_tz','company_id'):
ok=False
if ok:
def write(self, cr, uid, ids, values, context=None):
if ids == [uid]:
for key in values.keys():
if not (key in ('view', 'password','signature','action_id', 'company_id') or key.startswith('context_')):
break
else:
uid = 1
res = super(users, self).write(cr, uid, ids, values, *args, **argv)
res = super(users, self).write(cr, uid, ids, values, context=context)
self.company_get.clear_cache(cr.dbname)
# Restart the cache on the company_get method
self.pool.get('ir.model.access').call_cache_clearing_methods(cr)
+57
View File
@@ -0,0 +1,57 @@
- |
Safe_Eval Scenario:
In order to check that common dangerous operations are
not allowed by the safe_eval mechanism, attempt to
evaluate some bad expressions, and verify that it triggers
an error.
-
1. Try a few common expressions to verify they work with safe_eval
-
!python {model: ir.model}: |
from tools.safe_eval import safe_eval
expected = (1, {"a": 9 * 2}, (True, False, None))
actual = safe_eval('(1, {"a": 9 * 2}, (True, False, None))')
assert actual == expected, "Simple python expressions are not working with safe_eval"
-
2. Try simple literal definition to verify it works with literal_eval
-
!python {model: ir.model}: |
from tools.safe_eval import literal_eval
expected = (1, {"a": 9}, (True, False, None))
actual = literal_eval('(1, {"a": 9}, (True, False, None))')
assert actual == expected, "Simple python expressions are not working with literal_eval"
-
3. Try arithmetic expression in literal_eval to verify it does not work
-
!python {model: ir.model}: |
from tools.safe_eval import literal_eval
try:
literal_eval('(1, {"a": 2*9}, (True, False, None))')
assert False, "literal_eval should not accept arithmetic expressions"
except ValueError:
pass
-
4. Try forbidden expressions in literal_eval to verify they are not allowed
-
!python {model: ir.model}: |
from tools.safe_eval import literal_eval
try:
literal_eval('{"a": True.__class__}')
assert False, "literal_eval should accept only literals"
except ValueError:
pass
-
5. Try forbidden expressions in safe_eval to verify they are not allowed (open)
-
!python {model: ir.model}: |
from tools.safe_eval import safe_eval
try:
safe_eval('open("/etc/passwd","r")')
assert False, "safe_eval should not allow arbitrary expressions"
except NameError:
pass
except:
# NameError should be raised because open() builtin is not found,
# but other exceptions probably indicate that open() was executed!
assert False, "safe_eval should not allow arbitrary expressions"
+15 -9
View File
@@ -56,7 +56,7 @@ from tools.translate import _
import fields
import tools
from tools.safe_eval import safe_eval as eval
regex_order = re.compile('^(([a-z0-9_]+|"[a-z0-9_]+")( *desc| *asc)?( *, *|))+$', re.I)
@@ -353,8 +353,8 @@ def get_pg_type(f):
f_type = ('int4', 'INTEGER')
else:
f_type = ('varchar', 'VARCHAR(%d)' % f_size)
elif isinstance(f, fields.function) and eval('fields.'+(f._type)) in type_dict:
t = eval('fields.'+(f._type))
elif isinstance(f, fields.function) and eval('fields.'+(f._type),globals()) in type_dict:
t = eval('fields.'+(f._type), globals())
f_type = (type_dict[t], type_dict[t])
elif isinstance(f, fields.function) and f._type == 'float':
if f.digits:
@@ -1228,17 +1228,23 @@ class orm_template(object):
name = node.get('name')
default = self.default_get(cr, user, [name], context=context).get(name)
if default:
attrs['selection'] = relation.name_get(cr, 1, default, context=context)
attrs['selection'] = relation.name_get(cr, 1, [default], context=context)
else:
attrs['selection'] = []
# We can not use the 'string' domain has it is defined according to the record !
else:
# If domain and context are strings, we keep them for client-side, otherwise
# we evaluate them server-side to consider them when generating the list of
# possible values
# TODO: find a way to remove this hack, by allow dynamic domains
dom = []
if column._domain and not isinstance(column._domain, (str, unicode)):
if column._domain and not isinstance(column._domain, basestring):
dom = column._domain
dom += eval(node.get('domain','[]'), {'uid':user, 'time':time})
context.update(eval(node.get('context','{}')))
attrs['selection'] = relation._name_search(cr, user, '', dom, context=context, limit=None, name_get_uid=1)
search_context = dict(context)
if column._context and not isinstance(column._context, basestring):
search_context.update(column._context)
attrs['selection'] = relation._name_search(cr, user, '', dom, context=search_context, limit=None, name_get_uid=1)
if (node.get('required') and not int(node.get('required'))) or not column.required:
attrs['selection'].append((False,''))
fields[node.get('name')] = attrs
@@ -1951,9 +1957,9 @@ class orm_memory(orm_template):
f = True
for arg in result:
if arg[1] =='=':
val =eval('data[arg[0]]'+'==' +' arg[2]')
val =eval('data[arg[0]]'+'==' +' arg[2]', locals())
elif arg[1] in ['<','>','in','not in','<=','>=','<>']:
val =eval('data[arg[0]]'+arg[1] +' arg[2]')
val =eval('data[arg[0]]'+arg[1] +' arg[2]', locals())
elif arg[1] in ['ilike']:
if str(data[arg[0]]).find(str(arg[2]))!=-1:
val= True
+1
View File
@@ -24,6 +24,7 @@ import time
import netsvc
import tools
from tools.safe_eval import safe_eval as eval
import print_xml
import render
from interface import report_int
+2 -1
View File
@@ -23,6 +23,7 @@ import os,types
from lxml import etree
import netsvc
import tools
from tools.safe_eval import safe_eval
import print_fnc
import copy
from osv.orm import browse_null, browse_record
@@ -111,7 +112,7 @@ class document(object):
#Pinky: Why not this ? eval(expr, browser) ?
# name = browser.name
# data_dict = browser._data[self.get_value(browser, 'id')]
return eval(expr, {}, {'obj': record})
return safe_eval(expr, {}, {'obj': record})
def parse_node(self, node, parent, browser, datas=None):
attrs = self.node_attrs_get(node)
+1
View File
@@ -22,6 +22,7 @@
from report.interface import report_int
import pooler
import tools
from tools.safe_eval import safe_eval as eval
from lxml import etree
from report import render
import locale
+1
View File
@@ -34,6 +34,7 @@ import os
from lxml import etree
import base64
from reportlab.platypus.doctemplate import ActionFlowable
from tools.safe_eval import safe_eval as eval
encoding = 'utf-8'
+1
View File
@@ -42,6 +42,7 @@ from lxml import etree
import copy
import locale
import traceback, sys
from tools.safe_eval import safe_eval as eval
_regex = re.compile('\[\[(.+?)\]\]')
+2 -1
View File
@@ -23,6 +23,7 @@ import re
import reportlab
import reportlab.lib.units
from lxml import etree
from tools.safe_eval import safe_eval as eval
_regex = re.compile('\[\[(.+?)\]\]')
@@ -128,4 +129,4 @@ def attr_get(node, attrs, dict={}):
res[key] = unit_get(node.get(key))
return res
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+25 -18
View File
@@ -37,6 +37,12 @@ import pooler
from config import config
from yaml_import import convert_yaml_import
# Import of XML records requires the unsafe eval as well,
# almost everywhere, which is ok because it supposedly comes
# from trusted data, but at least we make it obvious now.
unsafe_eval = eval
from tools.safe_eval import safe_eval as eval
class ConvertError(Exception):
def __init__(self, doc, orig_excpt):
self.d = doc
@@ -62,7 +68,7 @@ def _eval_xml(self,node, pool, cr, uid, idref, context=None):
f_search = node.get("search",'').encode('utf-8')
f_use = node.get("use",'id').encode('ascii')
f_name = node.get("name",'').encode('utf-8')
q = eval(f_search, idref)
q = unsafe_eval(f_search, idref)
ids = pool.get(f_model).search(cr, uid, q)
if f_use != 'id':
ids = map(lambda x: x[f_use], pool.get(f_model).read(cr, uid, ids, [f_use]))
@@ -96,10 +102,10 @@ def _eval_xml(self,node, pool, cr, uid, idref, context=None):
pytz=pytzclass()
idref2['pytz'] = pytz
try:
return eval(a_eval, idref2)
except:
logger = netsvc.Logger()
logger.notifyChannel("init", netsvc.LOG_WARNING, 'could eval(%s) for %s in %s, please get back and fix it!' % (a_eval,node.get('name'),context))
return unsafe_eval(a_eval, idref2)
except Exception, e:
logger = logging.getLogger('init')
logger.warning('couldn\'t eval(%s) for %s in %s, please get back and fix it!' % (a_eval,node.get('name'),context), exc_info=True)
return ""
if t == 'xml':
def _process(s, idref):
@@ -144,7 +150,7 @@ def _eval_xml(self,node, pool, cr, uid, idref, context=None):
a_eval = node.get('eval','')
if a_eval:
idref['ref'] = lambda x: self.id_get(cr, False, x)
args = eval(a_eval, idref)
args = unsafe_eval(a_eval, idref)
for n in node:
return_val = _eval_xml(self,n, pool, cr, uid, idref, context)
if return_val is not None:
@@ -205,13 +211,13 @@ class xml_import(object):
def get_context(self, data_node, node, eval_dict):
data_node_context = (len(data_node) and data_node.get('context','').encode('utf8'))
if data_node_context:
context = eval(data_node_context, eval_dict)
context = unsafe_eval(data_node_context, eval_dict)
else:
context = {}
node_context = node.get("context",'').encode('utf8')
if node_context:
context.update(eval(node_context, eval_dict))
context.update(unsafe_eval(node_context, eval_dict))
return context
@@ -241,7 +247,7 @@ form: module.record_id""" % (xml_id,)
d_id = rec.get("id",'')
ids = []
if d_search:
ids = self.pool.get(d_model).search(cr,self.uid,eval(d_search))
ids = self.pool.get(d_model).search(cr, self.uid, unsafe_eval(d_search))
if d_id:
try:
ids.append(self.id_get(cr, d_model, d_id))
@@ -403,7 +409,8 @@ form: module.record_id""" % (xml_id,)
def ref(str_id):
return self.id_get(cr, None, str_id)
context = eval(context)
context = unsafe_eval(context)
# domain = eval(domain) # XXX need to test this line -> uid, active_id, active_ids, ...
res = {
@@ -623,7 +630,7 @@ form: module.record_id""" % (xml_id,)
if rec_id:
ids = [self.id_get(cr, rec_model, rec_id)]
elif rec_src:
q = eval(rec_src, eval_dict)
q = unsafe_eval(rec_src, eval_dict)
ids = self.pool.get(rec_model).search(cr, uid, q, context=context)
if rec_src_count:
count = int(rec_src_count)
@@ -651,14 +658,14 @@ form: module.record_id""" % (xml_id,)
if key in brrec:
return brrec[key]
return dict.__getitem__(self2, key)
globals = d()
globals['floatEqual'] = self._assert_equals
globals['ref'] = ref
globals['_ref'] = ref
globals_dict = d()
globals_dict['floatEqual'] = self._assert_equals
globals_dict['ref'] = ref
globals_dict['_ref'] = ref
for test in rec.findall('./test'):
f_expr = test.get("expr",'').encode('utf-8')
expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
expression_value = eval(f_expr, globals)
expression_value = unsafe_eval(f_expr, globals_dict)
if expression_value != expected_value: # assertion failed
self.assert_report.record_assertion(False, severity)
msg = 'assertion "%s" failed!\n' \
@@ -682,7 +689,7 @@ form: module.record_id""" % (xml_id,)
rec_id = rec.get("id",'').encode('ascii')
rec_context = rec.get("context", None)
if rec_context:
rec_context = eval(rec_context)
rec_context = unsafe_eval(rec_context)
self._test_xml_id(rec_id)
if self.isnoupdate(data_node) and self.mode != 'init':
# check if the xml record has an id string
@@ -722,7 +729,7 @@ form: module.record_id""" % (xml_id,)
f_val = False
if f_search:
q = eval(f_search, self.idref)
q = unsafe_eval(f_search, self.idref)
field = []
assert f_model, 'Define an attribute model="..." in your .XML file !'
f_obj = self.pool.get(f_model)
+237 -49
View File
@@ -1,66 +1,254 @@
# -*- 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.
###############################################################################
##############################################################################
__export_bis = {}
import sys
"""
safe_eval module - methods intended to provide more restricted alternatives to
evaluate simple and/or untrusted code.
def __init_ebis():
global __export_bis
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.
"""
_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', ]
# 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
if sys.version_info[0:2] >= (2,6):
_evars.extend(['bin', 'format', 'next'])
for v in _evars:
__export_bis[v] = __builtins__[v]
from opcode import HAVE_ARGUMENT, opmap, opname
from types import CodeType
__all__ = ['test_expr', 'literal_eval', 'safe_eval', 'const_eval', 'ext_eval' ]
_CONST_OPCODES = 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_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', '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 = []
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.
"""
try:
code_obj = compile(expr, "", mode)
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
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()
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.
return eval(expr,sglobals,slocals)
>>> 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)
#eof
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 safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
"""safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
System-restricted Python expression evaluation
Evaluates a string that contains an expression that mostly
uses Python constants, arithmetic expressions and the
objects directly provided in context.
This can be used to e.g. evaluate
an OpenERP domain expression from an untrusted source.
>>> safe_eval("__import__('sys').modules")
Traceback (most recent call last):
...
ValueError: opcode LOAD_NAME not allowed
"""
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__)')
if globals_dict is None:
globals_dict = {}
# 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()')
globals_dict = dict(globals_dict)
if locals_dict is not None:
locals_dict = dict(locals_dict)
globals_dict.update(
__builtins__ = {
'True': True,
'False': False,
'None': None,
'str': str,
'globals': locals,
'locals': locals,
'bool': bool,
'dict': dict,
'list': list,
'tuple': tuple,
}
)
return eval(test_expr(expr,_SAFE_OPCODES, mode=mode), globals_dict, locals_dict)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+10 -4
View File
@@ -8,9 +8,13 @@ import netsvc
import misc
from config import config
import yaml_tag
import yaml
# YAML import needs both safe and unsafe eval, but let's
# default to /safe/.
unsafe_eval = eval
from tools.safe_eval import safe_eval as eval
logger_channel = 'tests'
class YamlImportException(Exception):
@@ -234,8 +238,9 @@ class YamlInterpreter(object):
record = model.browse(self.cr, self.uid, id, context)
for test in expressions:
try:
success = eval(test, self.eval_context, RecordDictWrapper(record))
success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
except Exception, e:
self.logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
raise YamlImportAbortion(e)
if not success:
msg = 'Assertion "%s" FAILED\ntest: %s\n'
@@ -367,12 +372,13 @@ class YamlInterpreter(object):
code_context = {'model': model, 'cr': self.cr, 'uid': self.uid, 'log': log, 'context': self.context}
code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
try:
code = compile(statements, self.filename, 'exec')
eval(code, {'ref': self.get_id}, code_context)
code_obj = compile(statements, self.filename, 'exec')
unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
except AssertionError, e:
self._log_assert_failure(python.severity, 'AssertionError in Python code %s: %s', python.name, e)
return
except Exception, e:
self.logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
raise YamlImportAbortion(e)
else:
self.assert_report.record(True, python.severity)
+2 -1
View File
@@ -23,6 +23,7 @@ import sys
import netsvc
import osv as base
import pooler
from tools.safe_eval import safe_eval as eval
class Env(dict):
def __init__(self, cr, uid, model, ids):
@@ -54,7 +55,7 @@ def _eval_expr(cr, ident, workitem, action):
ret=False
else:
env = Env(cr, uid, model, ids)
ret = eval(line, env)
ret = eval(line, env, nocopy=True)
return ret
def execute_action(cr, ident, workitem, activity):