diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index d55061c3900..db93c12da22 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -31,6 +31,7 @@ import shutil import tempfile import urllib import urllib2 +import urlparse import zipfile import zipimport import lxml.html @@ -41,6 +42,7 @@ except ImportError: from StringIO import StringIO # NOQA import openerp +import openerp.exceptions from openerp import modules, tools, addons from openerp.modules.db import create_categories from openerp.tools.parse_version import parse_version @@ -620,40 +622,14 @@ class module(osv.osv): return res def download(self, cr, uid, ids, download=True, context=None): - res = [] - default_version = modules.adapt_version('1.0') - for mod in self.browse(cr, uid, ids, context=context): - if not mod.url: - continue - match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I) - version = default_version - if match: - version = match.group(1) - if parse_version(mod.installed_version) >= parse_version(version): - continue - res.append(mod.url) - if not download: - continue - zip_content = urllib.urlopen(mod.url).read() - fname = modules.get_module_path(str(mod.name) + '.zip', downloaded=True) - try: - with open(fname, 'wb') as fp: - fp.write(zip_content) - except Exception: - _logger.exception('Error when trying to create module ' - 'file %s', fname) - raise orm.except_orm(_('Error'), _('Can not create the module file:\n %s') % (fname,)) - terp = self.get_module_info(mod.name) - self.write(cr, uid, mod.id, self.get_values_from_terp(terp)) - cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s', (mod.id,)) - self._update_dependencies(cr, uid, mod, terp.get('depends', [])) - self._update_category(cr, uid, mod, terp.get('category', 'Uncategorized')) - # Import module - zimp = zipimport.zipimporter(fname) - zimp.load_module(mod.name) - return res + return [] def install_from_urls(self, cr, uid, urls, context=None): + if not self.pool['res.users'].has_group(cr, uid, 'base.group_system'): + raise openerp.exceptions.AccessDenied() + + apps_server = urlparse.urlparse(self.get_apps_server(cr, uid, context=context)) + OPENERP = 'openerp' tmp = tempfile.mkdtemp() _logger.debug('Install from url: %r', urls) @@ -662,6 +638,11 @@ class module(osv.osv): for module_name, url in urls.items(): if not url: continue # nothing to download, local version is already the last one + + up = urlparse.urlparse(url) + if up.scheme != apps_server.scheme or up.netloc != apps_server.netloc: + raise openerp.exceptions.AccessDenied() + try: _logger.info('Downloading module `%s` from OpenERP Apps', module_name) content = urllib2.urlopen(url).read() @@ -726,8 +707,8 @@ class module(osv.osv): finally: shutil.rmtree(tmp) - def install_by_names(self, cr, uid, names, context=None): - raise NotImplementedError('# TODO') + def get_apps_server(self, cr, uid, context=None): + return tools.config.get('apps_server', 'https://apps.openerp.com/apps') def _update_dependencies(self, cr, uid, mod_browse, depends=None): if depends is None: diff --git a/openerp/addons/base/static/src/js/apps.js b/openerp/addons/base/static/src/js/apps.js index e339ed2230e..115d35fcc4f 100644 --- a/openerp/addons/base/static/src/js/apps.js +++ b/openerp/addons/base/static/src/js/apps.js @@ -63,8 +63,8 @@ openerp.base = function(instance) { if (instance.base.apps_client) { return check_client_available(instance.base.apps_client); } else { - var ICP = new instance.web.Model('ir.config_parameter'); - return ICP.call('get_param', ['apps.server', 'https://apps.openerp.com/apps']).then(function(u) { + var Mod = new instance.web.Model('ir.module.module'); + return Mod.call('get_apps_server').then(function(u) { var link = $(_.str.sprintf('', u))[0]; var host = _.str.sprintf('%s//%s', link.protocol, link.host); var dbname = link.pathname; diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py index a89ae193f5c..bbdaa697bcd 100644 --- a/openerp/addons/base/tests/test_expression.py +++ b/openerp/addons/base/tests/test_expression.py @@ -183,7 +183,7 @@ class test_expression(common.TransactionCase): self.assertIn('res_partner_bank', sql_query[0], "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect main table") - expected = "%s like %s" % (unaccent('"res_partner_bank"."name"'), unaccent('%s')) + expected = "%s::text like %s" % (unaccent('"res_partner_bank"."name"'), unaccent('%s')) self.assertIn(expected, sql_query[1], "_auto_join off: ('bank_ids.name', 'like', '..') first query incorrect where condition") @@ -223,7 +223,7 @@ class test_expression(common.TransactionCase): self.assertIn('"res_partner_bank" as "res_partner__bank_ids"', sql_query[0], "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect join") - expected = "%s like %s" % (unaccent('"res_partner__bank_ids"."name"'), unaccent('%s')) + expected = "%s::text like %s" % (unaccent('"res_partner__bank_ids"."name"'), unaccent('%s')) self.assertIn(expected, sql_query[1], "_auto_join on: ('bank_ids.name', 'like', '..') query incorrect where condition") @@ -305,7 +305,7 @@ class test_expression(common.TransactionCase): self.assertIn('"res_country"', sql_query[0], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect main table") - expected = "%s like %s" % (unaccent('"res_country"."code"'), unaccent('%s')) + expected = "%s::text like %s" % (unaccent('"res_country"."code"'), unaccent('%s')) self.assertIn(expected, sql_query[1], "_auto_join on for state_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition") @@ -339,7 +339,7 @@ class test_expression(common.TransactionCase): self.assertIn('"res_country" as "res_country_state__country_id"', sql_query[0], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect join") - expected = "%s like %s" % (unaccent('"res_country_state__country_id"."code"'), unaccent('%s')) + expected = "%s::text like %s" % (unaccent('"res_country_state__country_id"."code"'), unaccent('%s')) self.assertIn(expected, sql_query[1], "_auto_join on for country_id: ('state_id.country_id.code', 'like', '..') query 1 incorrect where condition") @@ -373,7 +373,7 @@ class test_expression(common.TransactionCase): self.assertIn('"res_country" as "res_partner__state_id__country_id"', sql_query[0], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect join") - expected = "%s like %s" % (unaccent('"res_partner__state_id__country_id"."code"'), unaccent('%s')) + expected = "%s::text like %s" % (unaccent('"res_partner__state_id__country_id"."code"'), unaccent('%s')) self.assertIn(expected, sql_query[1], "_auto_join on: ('state_id.country_id.code', 'like', '..') query incorrect where condition") @@ -403,7 +403,7 @@ class test_expression(common.TransactionCase): # Test produced queries that domains effectively present sql_query = self.query_list[0].get_sql() - expected = "%s like %s" % (unaccent('"res_partner__child_ids__bank_ids"."acc_number"'), unaccent('%s')) + expected = "%s::text like %s" % (unaccent('"res_partner__child_ids__bank_ids"."acc_number"'), unaccent('%s')) self.assertIn(expected, sql_query[1], "_auto_join on one2many with domains incorrect result") # TDE TODO: check first domain has a correct table name diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 52dc4a265ba..d3231c54462 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -1182,14 +1182,15 @@ class expression(object): else: need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike') sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator) + cast = '::text' if sql_operator.endswith('like') else '' if left in model._columns: format = need_wildcard and '%s' or model._columns[left]._symbol_set[0] unaccent = self._unaccent if sql_operator.endswith('like') else lambda x: x column = '%s.%s' % (table_alias, _quote(left)) - query = '(%s %s %s)' % (unaccent(column), sql_operator, unaccent(format)) + query = '(%s%s %s %s)' % (unaccent(column), cast, sql_operator, unaccent(format)) elif left in MAGIC_COLUMNS: - query = "(%s.\"%s\" %s %%s)" % (table_alias, left, sql_operator) + query = "(%s.\"%s\"%s %s %%s)" % (table_alias, left, cast, sql_operator) params = right else: # Must not happen raise ValueError("Invalid field %r in domain term %r" % (left, leaf)) diff --git a/openerp/sql_db.py b/openerp/sql_db.py index 694ae02b3d9..3eb50d84494 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -167,7 +167,7 @@ class Cursor(object): self.sql_log_count = 0 self.__closed = True # avoid the call of close() (by __del__) if an exception # is raised by any of the following initialisations - self._pool = pool + self.__pool = pool self.dbname = dbname # Whether to enable snapshot isolation level for this cursor. @@ -313,7 +313,7 @@ class Cursor(object): chosen_template = tools.config['db_template'] templates_list = tuple(set(['template0', 'template1', 'postgres', chosen_template])) keep_in_pool = self.dbname not in templates_list - self._pool.give_back(self._cnx, keep_in_pool=keep_in_pool) + self.__pool.give_back(self._cnx, keep_in_pool=keep_in_pool) @check def autocommit(self, on): @@ -484,12 +484,12 @@ class Connection(object): def __init__(self, pool, dbname): self.dbname = dbname - self._pool = pool + self.__pool = pool def cursor(self, serialized=True): cursor_type = serialized and 'serialized ' or '' _logger.debug('create %scursor to %r', cursor_type, self.dbname) - return Cursor(self._pool, self.dbname, serialized=serialized) + return Cursor(self.__pool, self.dbname, serialized=serialized) # serialized_cursor is deprecated - cursors are serialized by default serialized_cursor = cursor diff --git a/openerp/tools/safe_eval.py b/openerp/tools/safe_eval.py index 508a072a3b2..2f280685350 100644 --- a/openerp/tools/safe_eval.py +++ b/openerp/tools/safe_eval.py @@ -71,7 +71,8 @@ _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [ 'CONTINUE_LOOP', 'RAISE_VARARGS', # New in Python 2.7 - http://bugs.python.org/issue4715 : 'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE', - 'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY' + 'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY', 'LOAD_FAST', + 'LOAD_GLOBAL', # Only allows access to restricted globals ] if x in opmap)) _logger = logging.getLogger(__name__) @@ -86,16 +87,65 @@ def _get_opcodes(codeobj): [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) + yield code + if code >= HAVE_ARGUMENT: i += 3 else: i += 1 - return opcodes + +def assert_no_dunder_name(code_obj, expr): + """ assert_no_dunder_name(code_obj, expr) -> None + + Asserts that the code object does not refer to any "dunder name" + (__$name__), so that safe_eval prevents access to any internal-ish Python + attribute or method (both are loaded via LOAD_ATTR which uses a name, not a + const or a var). + + Checks that no such name exists in the provided code object (co_names). + + :param code_obj: code object to name-validate + :type code_obj: CodeType + :param str expr: expression corresponding to the code object, for debugging + purposes + :raises NameError: in case a forbidden name (containing two underscores) + is found in ``code_obj`` + + .. note:: actually forbids every name containing 2 underscores + """ + for name in code_obj.co_names: + if "__" in name: + raise NameError('Access to forbidden name %r (%r)' % (name, expr)) + +def assert_valid_codeobj(allowed_codes, code_obj, expr): + """ Asserts that the provided code object validates against the bytecode + and name constraints. + + Recursively validates the code objects stored in its co_consts in case + lambdas are being created/used (lambdas generate their own separated code + objects and don't live in the root one) + + :param allowed_codes: list of permissible bytecode instructions + :type allowed_codes: set(int) + :param code_obj: code object to name-validate + :type code_obj: CodeType + :param str expr: expression corresponding to the code object, for debugging + purposes + :raises ValueError: in case of forbidden bytecode in ``code_obj`` + :raises NameError: in case a forbidden name (containing two underscores) + is found in ``code_obj`` + """ + assert_no_dunder_name(code_obj, expr) + for opcode in _get_opcodes(code_obj): + if opcode not in allowed_codes: + raise ValueError( + "opcode %s not allowed (%r)" % (opname[opcode], expr)) + for const in code_obj.co_consts: + if isinstance(const, CodeType): + assert_valid_codeobj(allowed_codes, const, 'lambda') def test_expr(expr, allowed_codes, mode="eval"): """test_expr(expression, allowed_codes[, mode]) -> code_object @@ -110,15 +160,13 @@ def test_expr(expr, allowed_codes, mode="eval"): # eval() does not like leading/trailing whitespace expr = expr.strip() code_obj = compile(expr, "", mode) - except (SyntaxError, TypeError): + except (SyntaxError, TypeError, ValueError): raise except Exception, e: import sys exc_info = sys.exc_info() raise ValueError, '"%s" while compiling\n%r' % (ustr(e), expr), exc_info[2] - for code in _get_opcodes(code_obj): - if code not in allowed_codes: - raise ValueError("opcode %s not allowed (%r)" % (opname[code], expr)) + assert_valid_codeobj(allowed_codes, code_obj, expr) return code_obj @@ -187,19 +235,13 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal This can be used to e.g. evaluate an OpenERP domain expression from an untrusted source. - Throws TypeError, SyntaxError or ValueError (not allowed) accordingly. - - >>> safe_eval("__import__('sys').modules") - Traceback (most recent call last): - ... - ValueError: opcode LOAD_NAME not allowed - + :throws TypeError: If the expression provided is a code object + :throws SyntaxError: If the expression provided is not valid Python + :throws NameError: If the expression provided accesses forbidden names + :throws ValueError: If the expression provided uses forbidden bytecode """ 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__)') + raise TypeError("safe_eval does not allow direct evaluation of code objects.") if globals_dict is None: globals_dict = {} diff --git a/setup.py b/setup.py index c776b910a8c..24c1df899d1 100644 --- a/setup.py +++ b/setup.py @@ -136,7 +136,7 @@ setuptools.setup( 'python-openid', 'pytz', 'pyusb >= 1.0.0b1', - 'pywebdav', + 'pywebdav < 0.9.8', 'pyyaml', 'qrcode', 'reportlab', # windows binary pypi.python.org/pypi/reportlab