diff --git a/CHANGELOG b/CHANGELOG index fae5ead988..66d7b9c18e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add new tool safe_eval * Handle sequence and history renaming when renaming table * Add old_password to set_preferences of res.user on password change * Allow to drop constraint, index and foreign key with custom table name diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py index 9982f55f1f..38da3f5375 100644 --- a/trytond/backend/postgresql/database.py +++ b/trytond/backend/postgresql/database.py @@ -205,6 +205,7 @@ class Database(DatabaseInterface): @staticmethod def init(cursor): + from trytond.tools import safe_eval sql_file = os.path.join(os.path.dirname(__file__), 'init.sql') for line in file(sql_file).read().split(';'): if (len(line)>0) and (not line.isspace()): @@ -214,7 +215,7 @@ class Database(DatabaseInterface): root_path = os.path.join(os.path.dirname(__file__), '..', '..') tryton_file = os.path.join(root_path, i, '__tryton__.py') mod_path = os.path.join(root_path, i) - info = eval(file(tryton_file).read()) + info = safe_eval(file(tryton_file).read()) active = info.get('active', False) if active: state = 'to install' diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py index a5e1794c79..f9466d1216 100644 --- a/trytond/backend/sqlite/database.py +++ b/trytond/backend/sqlite/database.py @@ -162,6 +162,7 @@ class Database(DatabaseInterface): @staticmethod def init(cursor): + from trytond.tools import safe_eval sql_file = os.path.join(os.path.dirname(__file__), 'init.sql') for line in file(sql_file).read().split(';'): if (len(line)>0) and (not line.isspace()): @@ -171,7 +172,7 @@ class Database(DatabaseInterface): root_path = os.path.join(os.path.dirname(__file__), '..', '..') tryton_file = os.path.join(root_path, i, '__tryton__.py') mod_path = os.path.join(root_path, i) - info = eval(file(tryton_file).read()) + info = safe_eval(file(tryton_file).read()) active = info.get('active', False) if active: state = 'to install' diff --git a/trytond/ir/cron.py b/trytond/ir/cron.py index 8555972ae9..47aa58d74d 100644 --- a/trytond/ir/cron.py +++ b/trytond/ir/cron.py @@ -10,6 +10,7 @@ SPEC: Execute "model.function(*eval(args))" periodically from trytond.backend import Database from trytond.model import ModelView, ModelSQL, fields +from trytond.tools import safe_eval from mx import DateTime import datetime import traceback @@ -98,7 +99,7 @@ class Cron(ModelSQL, ModelView): def _callback(self, cursor, cron): try: - args = (cron['args'] or []) and eval(cron['args']) + args = (cron['args'] or []) and safe_eval(cron['args']) obj = self.pool.get(cron['model']) if not obj and hasattr(obj, cron['function']): return False diff --git a/trytond/ir/default.py b/trytond/ir/default.py index 42a1099a20..deb9007c2e 100644 --- a/trytond/ir/default.py +++ b/trytond/ir/default.py @@ -2,7 +2,7 @@ #this repository contains the full copyright notices and license terms. "Default" from trytond.model import ModelView, ModelSQL, fields -from trytond.tools import Cache +from trytond.tools import Cache, safe_eval from decimal import Decimal import mx.DateTime import datetime @@ -69,7 +69,7 @@ class Default(ModelSQL, ModelView): date.month, date.day, date.hour, date.minute, int(date.second)) else: - res[default.field.name] = eval(default.value) + res[default.field.name] = safe_eval(default.value) return res get_default = Cache('ir_default.get_default')(get_default) diff --git a/trytond/ir/lang.py b/trytond/ir/lang.py index 2ae0ea5dd2..dd56697776 100644 --- a/trytond/ir/lang.py +++ b/trytond/ir/lang.py @@ -3,6 +3,7 @@ "Lang" from trytond.model import ModelView, ModelSQL, fields from trytond.model.cacheable import Cacheable +from trytond.tools import safe_eval import time from time_locale import TIME_LOCALE @@ -99,7 +100,7 @@ class Lang(ModelSQL, ModelView, Cacheable): ''' for lang in self.browse(cursor, user, ids): try: - grouping = eval(lang.grouping) + grouping = safe_eval(lang.grouping) for i in grouping: if not isinstance(i, int): return False @@ -174,10 +175,10 @@ class Lang(ModelSQL, ModelView, Cacheable): # Code from _group in locale.py if monetary: thousands_sep = monetary['mon_thousands_sep'] - grouping = eval(monetary['mon_grouping']) + grouping = safe_eval(monetary['mon_grouping']) else: thousands_sep = lang['thousands_sep'] - grouping = eval(lang['grouping']) + grouping = safe_eval(lang['grouping']) if not grouping: return (s, 0) result = "" diff --git a/trytond/ir/module/module.py b/trytond/ir/module/module.py index 07b597cf97..342b8cb44e 100644 --- a/trytond/ir/module/module.py +++ b/trytond/ir/module/module.py @@ -181,7 +181,7 @@ class Module(ModelSQL, ModelView): else: file_p = tools.file_open(os.path.join(name, '__tryton__.py')) data = file_p.read() - info = eval(data) + info = tools.safe_eval(data) file_p.close() except: return {} diff --git a/trytond/ir/rule.py b/trytond/ir/rule.py index b762d1a106..4c98b3e92e 100644 --- a/trytond/ir/rule.py +++ b/trytond/ir/rule.py @@ -2,7 +2,7 @@ #this repository contains the full copyright notices and license terms. "Rule" from trytond.model import ModelView, ModelSQL, fields -from trytond.tools import Cache +from trytond.tools import Cache, safe_eval import time @@ -188,7 +188,7 @@ class Rule(ModelSQL, ModelView): recur=['many2one'], root_tech='user', root='User') # Use root user without context to prevent recursion for rule in self.browse(cursor, 0, ids): - dom = eval("[('%s', '%s', %s)]" % \ + dom = safe_eval("[('%s', '%s', %s)]" % \ (rule.field.name, rule.operator, operand2query[rule.operand]), {'user': self.pool.get('res.user').browse(cursor, 0, diff --git a/trytond/model/browse.py b/trytond/model/browse.py index 0a4dbb0b92..eec0514940 100644 --- a/trytond/model/browse.py +++ b/trytond/model/browse.py @@ -272,12 +272,12 @@ class EvalEnvironment(dict): def __getattr__(self, item): return self.__getitem__(item) - def get(self, item): + def get(self, item, default=None): try: return self.__getitem__(item) except: pass - return super(EvalEnvironment, self).get(item) + return super(EvalEnvironment, self).get(item, default=default) def __nonzero__(self): return bool(self._record) diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py index 015a5bc0d9..565c435da1 100644 --- a/trytond/model/fields/reference.py +++ b/trytond/model/fields/reference.py @@ -2,6 +2,7 @@ #this repository contains the full copyright notices and license terms. from trytond.model.fields.field import Field +from trytond.tools import safe_eval class Reference(Field): @@ -63,7 +64,7 @@ class Reference(Field): ref_obj = model.pool.get(ref_model) ref_id_found.setdefault(ref_model, set()) try: - ref_id = eval(ref_id) + ref_id = safe_eval(ref_id) except: pass try: diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index 558564b69b..cacf10ce94 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -4,6 +4,7 @@ from trytond.model import Model from trytond.model import fields from trytond.model.browse import BrowseRecordList, BrowseRecord, BrowseRecordNull from trytond.model.browse import EvalEnvironment +from trytond.tools import safe_eval import datetime import time from decimal import Decimal @@ -813,7 +814,7 @@ class ModelStorage(Model): context=context): if not line.values: continue - xml_values = eval(line.values, { + xml_values = safe_eval(line.values, { 'Decimal': Decimal, 'datetime': datetime, }) @@ -920,7 +921,7 @@ class ModelStorage(Model): else: if not env: return True - res.append(eval(arg, env)) + res.append(safe_eval(arg, env)) elif isinstance(arg, tuple): res.append(arg) elif isinstance(arg, list): @@ -1002,7 +1003,7 @@ class ModelStorage(Model): env['time'] = time env['context'] = context env['active_id'] = record.id - required = eval(field.states['required'], env) + required = safe_eval(field.states['required'], env) if required and not record[field_name]: self.raise_user_error(cursor, 'required_validation_record', diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py index 242b3d7ecc..d743bc6f5d 100644 --- a/trytond/model/modelview.py +++ b/trytond/model/modelview.py @@ -1,7 +1,7 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. from trytond.model import Model -from trytond.tools import Cache +from trytond.tools import Cache, safe_eval from trytond.modules import create_graph, get_module_list from lxml import etree try: @@ -179,7 +179,7 @@ class ModelView(Model): raise_p = True for arch, domain, _ in sql_inherit: if domain: - if not eval(domain, {'context': context}): + if not safe_eval(domain, {'context': context}): continue if not arch or not arch.strip(): continue diff --git a/trytond/modules/__init__.py b/trytond/modules/__init__.py index 739859d626..659d30fcef 100644 --- a/trytond/modules/__init__.py +++ b/trytond/modules/__init__.py @@ -144,7 +144,8 @@ def create_graph(module_list, force=None): mod_path = OPJ(ep.dist.location, 'trytond', 'modules', module) if os.path.isfile(tryton_file) or zipfile.is_zipfile(mod_path+'.zip'): try: - info = eval(tools.file_open(tryton_file, subdir='').read()) + info = tools.safe_eval(tools.file_open(tryton_file, + subdir='').read()) except: logger.error('%s:eval file %s' % (module, tryton_file)) raise diff --git a/trytond/res/user.py b/trytond/res/user.py index bd38a2fdf1..dd898af924 100644 --- a/trytond/res/user.py +++ b/trytond/res/user.py @@ -5,7 +5,7 @@ import copy from trytond.model import ModelView, ModelSQL, fields from trytond.wizard import Wizard from lxml import etree -from trytond.tools import Cache +from trytond.tools import Cache, safe_eval from trytond.backend import TableHandler from trytond.security import get_connections import string @@ -257,7 +257,7 @@ class User(ModelSQL, ModelView): date = date.replace(i, j) res['locale'] = { 'date': date, - 'grouping': eval(user.language.grouping), + 'grouping': safe_eval(user.language.grouping), 'decimal_point': user.language.decimal_point, 'thousands_sep': user.language.thousands_sep, } diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py index 9c57df30ac..25c1996e40 100644 --- a/trytond/tests/test_tryton.py +++ b/trytond/tests/test_tryton.py @@ -12,7 +12,7 @@ import unittest import time from decimal import Decimal from trytond import pysocket -from trytond.tools import reduce_ids +from trytond.tools import reduce_ids, safe_eval from lxml import etree ADMIN_PASSWORD = 'admin' @@ -102,6 +102,32 @@ class ToolsTestCase(unittest.TestCase): reduce_ids('id', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 18, 19, 21])) + def test0060safe_eval_builtin(self): + ''' + Attempt to access a unsafe builtin. + ''' + self.assertRaises(Exception, safe_eval, "open('test.txt', 'w')") + + def test0061safe_eval_getattr(self): + ''' + Attempt to get arround direct attr access. + ''' + self.assertRaises(Exception, safe_eval, "getattr(int, '__abs__')") + + def test0062safe_eval_func_globals(self): + ''' + Attempt to access global enviroment where fun was defined. + ''' + self.assertRaises(Exception, safe_eval, + "def x(): pass; print x.func_globals") + + def test0063safe_eval_lowlevel(self): + ''' + Lowlevel tricks to access 'object'. + ''' + self.assertRaises(Exception, safe_eval, + "().__class__.mro()[1].__subclasses__()") + class FieldsTestCase(unittest.TestCase): ''' diff --git a/trytond/tools/convert.py b/trytond/tools/convert.py index 8e0a090358..92b75f49dd 100644 --- a/trytond/tools/convert.py +++ b/trytond/tools/convert.py @@ -9,6 +9,7 @@ except ImportError: import os.path from trytond.config import CONFIG from trytond.version import VERSION +from trytond.tools import safe_eval import time from xml import sax from decimal import Decimal @@ -54,7 +55,7 @@ class MenuitemTagHandler: values[attr] = attributes.get(attr) if attributes.get('active'): - values['active'] = bool(eval(attributes['active'])) + values['active'] = bool(safe_eval(attributes['active'])) if values.get('parent'): values['parent'] = self.mh.get_id(values['parent']) @@ -201,7 +202,8 @@ class RecordTagHandler: f_obj = self.mh.pool.get(search_model) answer = f_obj.browse( self.mh.cursor, self.mh.user, - f_obj.search(self.mh.cursor, self.mh.user, eval(search_attr), + f_obj.search(self.mh.cursor, self.mh.user, + safe_eval(search_attr), context={'active_test': False})) if not answer: return @@ -221,7 +223,7 @@ class RecordTagHandler: context['version'] = VERSION.rsplit('.', 1)[0] context['ref'] = self.mh.get_id context['obj'] = lambda *a: 1 - self.values[field_name] = eval(eval_attr, context) + self.values[field_name] = safe_eval(eval_attr, context) else: raise Exception("Tags '%s' not supported inside tag record."% (name,)) @@ -511,7 +513,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler): return False ref_mode, ref_id = browse_record[key].split(',', 1) try: - ref_id = eval(ref_id) + ref_id = safe_eval(ref_id) except: pass if isinstance(ref_id, (list, tuple)): @@ -581,7 +583,7 @@ class TrytondXmlHandler(sax.handler.ContentHandler): if not old_values: old_values = {} else: - old_values = eval(old_values, { + old_values = safe_eval(old_values, { 'Decimal': Decimal, 'datetime': datetime, }) diff --git a/trytond/tools/misc.py b/trytond/tools/misc.py index 09ca6d79c3..42c5f78f30 100644 --- a/trytond/tools/misc.py +++ b/trytond/tools/misc.py @@ -18,6 +18,7 @@ try: import cStringIO as StringIO except ImportError: import StringIO +import dis def find_in_path(name): if os.name == "nt": @@ -492,3 +493,45 @@ def reduce_ids(field, ids): ','.join(('%s',) * len(discontinue_list)) + '))') args.extend(discontinue_list) return '(' + ' OR '.join(sql) + ')', args + +_ALLOWED_CODES = set(dis.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', + '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_LSHIFT','BINARY_RSHIFT','BINARY_AND','BINARY_XOR', 'BINARY_OR', + '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', 'BINARY_SUBSCR', + ] if x in dis.opmap) + + +def safe_eval(source, data=None): + if '__subclasses__' in source: + raise ValueError('__subclasses__ not allowed') + c = compile(source, '', 'eval') + codes = [] + s = c.co_code + i = 0 + while i < len(s): + code = ord(s[i]) + codes.append(code) + if code >= dis.HAVE_ARGUMENT: + i += 3 + else: + i += 1 + for code in codes: + if code not in _ALLOWED_CODES: + raise ValueError('opcode %s not allowed' % dis.opname[code]) + return eval(c, {'__builtins__': { + 'True': True, + 'False': False, + 'str': str, + 'globals': locals, + 'locals': locals, + 'bool': bool, + 'dict': dict, + }}, data) diff --git a/trytond/webdav/webdav.py b/trytond/webdav/webdav.py index 231a601392..63f6c50413 100644 --- a/trytond/webdav/webdav.py +++ b/trytond/webdav/webdav.py @@ -7,7 +7,7 @@ import time from trytond.model import ModelView, ModelSQL, fields from trytond.version import PACKAGE, VERSION, WEBSITE from trytond.report import Report -from trytond.tools import reduce_ids +from trytond.tools import reduce_ids, safe_eval class Collection(ModelSQL, ModelView): @@ -271,7 +271,7 @@ class Collection(ModelSQL, ModelView): if not model_obj: return res model_ids = model_obj.search(cursor, user, - eval(collection.domain or "[]"), context=context) + safe_eval(collection.domain or "[]"), context=context) for child in model_obj.browse(cursor, user, model_ids, context=context): if '/' in child.rec_name: diff --git a/trytond/workflow/expr.py b/trytond/workflow/expr.py index b62949c528..b5c338d662 100644 --- a/trytond/workflow/expr.py +++ b/trytond/workflow/expr.py @@ -1,6 +1,7 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. from trytond.pool import Pool +from trytond.tools import safe_eval class EnvCall(object): @@ -55,7 +56,7 @@ def eval_expr(cursor, user, model, obj_id, action, context=None): elif line =='False': res = False else: - res = eval(line, env) + res = safe_eval(line, env) return res def execute(cursor, user, model, obj_id, activity, context=None):