diff --git a/bin/addons/__init__.py b/bin/addons/__init__.py index ac8e951e2a3..a70468cf071 100644 --- a/bin/addons/__init__.py +++ b/bin/addons/__init__.py @@ -28,6 +28,7 @@ import zipimport import osv import tools import tools.osutil +from tools.safe_eval import safe_eval as eval import pooler diff --git a/bin/addons/base/__openerp__.py b/bin/addons/base/__openerp__.py index 2308ecfccc8..26b2869758e 100644 --- a/bin/addons/base/__openerp__.py +++ b/bin/addons/base/__openerp__.py @@ -75,6 +75,7 @@ ], 'test': [ 'test/base_test.xml', + #'test/base_test.yml' 'test/test_context.xml', 'bug_lp541545.xml', ], diff --git a/bin/addons/base/base_update.xml b/bin/addons/base/base_update.xml index 04d07efeafa..b93e10af2d6 100644 --- a/bin/addons/base/base_update.xml +++ b/bin/addons/base/base_update.xml @@ -78,7 +78,8 @@
- + @@ -110,7 +111,7 @@ - + diff --git a/bin/addons/base/i18n/hr.po b/bin/addons/base/i18n/hr.po index 6dd69ebf20c..0d4ec1afbbe 100644 --- a/bin/addons/base/i18n/hr.po +++ b/bin/addons/base/i18n/hr.po @@ -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 \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" diff --git a/bin/addons/base/i18n/ja.po b/bin/addons/base/i18n/ja.po index b594dc7e796..fd5e2dddf5e 100644 --- a/bin/addons/base/i18n/ja.po +++ b/bin/addons/base/i18n/ja.po @@ -8,13 +8,13 @@ msgstr "" "Project-Id-Version: openobject-server\n" "Report-Msgid-Bugs-To: FULL NAME \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) \n" "Language-Team: Japanese \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 diff --git a/bin/addons/base/i18n/sv.po b/bin/addons/base/i18n/sv.po index c31bb344e3a..9be71a5771c 100644 --- a/bin/addons/base/i18n/sv.po +++ b/bin/addons/base/i18n/sv.po @@ -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 \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 diff --git a/bin/addons/base/i18n/uk.po b/bin/addons/base/i18n/uk.po index 8ba96ebbdbd..e8feacccf0c 100644 --- a/bin/addons/base/i18n/uk.po +++ b/bin/addons/base/i18n/uk.po @@ -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) \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 diff --git a/bin/addons/base/i18n/zh_CN.po b/bin/addons/base/i18n/zh_CN.po index 58fb80b19ad..75561ac26da 100644 --- a/bin/addons/base/i18n/zh_CN.po +++ b/bin/addons/base/i18n/zh_CN.po @@ -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 \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 diff --git a/bin/addons/base/ir/ir_actions.py b/bin/addons/base/ir/ir_actions.py index 6c51ab2f771..c3e9945df96 100644 --- a/bin/addons/base/ir/ir_actions.py +++ b/bin/addons/base/ir/ir_actions.py @@ -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: diff --git a/bin/addons/base/ir/ir_cron.py b/bin/addons/base/ir/ir_cron.py index 66ead94be48..905b2dc1778 100644 --- a/bin/addons/base/ir/ir_cron.py +++ b/bin/addons/base/ir/ir_cron.py @@ -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 diff --git a/bin/addons/base/ir/ir_report_custom.py b/bin/addons/base/ir/ir_report_custom.py index 3b20d193d8b..036469fb417 100644 --- a/bin/addons/base/ir/ir_report_custom.py +++ b/bin/addons/base/ir/ir_report_custom.py @@ -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): diff --git a/bin/addons/base/ir/ir_rule.py b/bin/addons/base/ir/ir_rule.py index de465fa8025..ba508b48920 100644 --- a/bin/addons/base/ir/ir_rule.py +++ b/bin/addons/base/ir/ir_rule.py @@ -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' diff --git a/bin/addons/base/ir/ir_ui_view.py b/bin/addons/base/ir/ir_ui_view.py index f1a50143ef3..bc395325840 100644 --- a/bin/addons/base/ir/ir_ui_view.py +++ b/bin/addons/base/ir/ir_ui_view.py @@ -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 diff --git a/bin/addons/base/res/res_company.py b/bin/addons/base/res/res_company.py index 7d8a9cda8f3..c076eef612a 100644 --- a/bin/addons/base/res/res_company.py +++ b/bin/addons/base/res/res_company.py @@ -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) diff --git a/bin/addons/base/res/res_lang.py b/bin/addons/base/res/res_lang.py index 66c1463eba9..13c4a33aa9c 100644 --- a/bin/addons/base/res/res_lang.py +++ b/bin/addons/base/res/res_lang.py @@ -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" diff --git a/bin/addons/base/res/res_user.py b/bin/addons/base/res/res_user.py index 3439a00c2cb..9040f90be95 100644 --- a/bin/addons/base/res/res_user.py +++ b/bin/addons/base/res/res_user.py @@ -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) diff --git a/bin/addons/base/test/base_test.yml b/bin/addons/base/test/base_test.yml new file mode 100644 index 00000000000..81a818f80c2 --- /dev/null +++ b/bin/addons/base/test/base_test.yml @@ -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" + diff --git a/bin/osv/orm.py b/bin/osv/orm.py index d6e0e6b1395..b166881798b 100644 --- a/bin/osv/orm.py +++ b/bin/osv/orm.py @@ -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 diff --git a/bin/report/custom.py b/bin/report/custom.py index d4309b31ab9..b53be34cfdb 100644 --- a/bin/report/custom.py +++ b/bin/report/custom.py @@ -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 diff --git a/bin/report/print_xml.py b/bin/report/print_xml.py index 2c24d3766f3..f349e10b1ba 100644 --- a/bin/report/print_xml.py +++ b/bin/report/print_xml.py @@ -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) diff --git a/bin/report/printscreen/ps_list.py b/bin/report/printscreen/ps_list.py index 0dea67382fe..4eba7372f2d 100644 --- a/bin/report/printscreen/ps_list.py +++ b/bin/report/printscreen/ps_list.py @@ -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 diff --git a/bin/report/render/rml2pdf/trml2pdf.py b/bin/report/render/rml2pdf/trml2pdf.py index 0c8e8e751e3..6628817ff9d 100644 --- a/bin/report/render/rml2pdf/trml2pdf.py +++ b/bin/report/render/rml2pdf/trml2pdf.py @@ -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' diff --git a/bin/report/render/rml2pdf/utils.py b/bin/report/render/rml2pdf/utils.py index ac4370ab204..ca51f136b6d 100644 --- a/bin/report/render/rml2pdf/utils.py +++ b/bin/report/render/rml2pdf/utils.py @@ -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('\[\[(.+?)\]\]') diff --git a/bin/report/render/rml2txt/utils.py b/bin/report/render/rml2txt/utils.py index dc7651ea2ad..abbf2dc8426 100644 --- a/bin/report/render/rml2txt/utils.py +++ b/bin/report/render/rml2txt/utils.py @@ -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: \ No newline at end of file +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/bin/tools/convert.py b/bin/tools/convert.py index a7e002e1a61..6f1a4e5996a 100644 --- a/bin/tools/convert.py +++ b/bin/tools/convert.py @@ -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) diff --git a/bin/tools/safe_eval.py b/bin/tools/safe_eval.py index a7fce27d2e0..bd8e8ec04d6 100644 --- a/bin/tools/safe_eval.py +++ b/bin/tools/safe_eval.py @@ -1,66 +1,254 @@ # -*- 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. -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='', 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: diff --git a/bin/tools/yaml_import.py b/bin/tools/yaml_import.py index df825f09cbb..adbb42790e5 100644 --- a/bin/tools/yaml_import.py +++ b/bin/tools/yaml_import.py @@ -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) diff --git a/bin/workflow/wkf_expr.py b/bin/workflow/wkf_expr.py index 98896df9324..0150105c3ab 100644 --- a/bin/workflow/wkf_expr.py +++ b/bin/workflow/wkf_expr.py @@ -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):