diff -Nru swift-1.4.8/debian/changelog swift-1.4.8/debian/changelog --- swift-1.4.8/debian/changelog 2014-03-14 18:23:19.000000000 +0000 +++ swift-1.4.8/debian/changelog 2015-07-27 16:17:55.000000000 +0000 @@ -1,3 +1,24 @@ +swift (1.4.8-0ubuntu2.5) precise-security; urgency=medium + + [ Marc Deslauriers ] + * SECURITY UPDATE: metadata constraint bypass via multiple requests + - debian/patches/CVE-2014-7960.patch: add metadata checks to + swift/account/server.py, swift/common/constraints.py, + swift/common/db.py, swift/container/server.py, added tests to + test/unit/common/test_db.py, + test/functionalnosetests/test_account.py, + test/functionalnosetests/test_container.py. + - CVE-2014-7960 + + [ Jamie Strandboge ] + * debian/patches/CVE-2014-7960.patch: + - adjust unittests since we use webob.exc and not the newer swob + - adjust functional tests to properly skip if test environment is not + specified and to not interfere with other functional tests + * debian/control: Build-Depends on python-mock + + -- Jamie Strandboge Mon, 27 Jul 2015 10:48:47 -0500 + swift (1.4.8-0ubuntu2.4) precise-security; urgency=medium * SECURITY UPDATE: timing side-channel attack in TempURL diff -Nru swift-1.4.8/debian/control swift-1.4.8/debian/control --- swift-1.4.8/debian/control 2012-04-10 13:23:59.000000000 +0000 +++ swift-1.4.8/debian/control 2015-07-23 23:18:52.000000000 +0000 @@ -21,7 +21,8 @@ python-nose, python-paste, python-pastedeploy, - python-sphinx (>= 1.0) + python-sphinx (>= 1.0), + python-mock Standards-Version: 3.9.2 Homepage: http://launchpad.net/swift Vcs-Browser: http://bazaar.launchpad.net/~ubuntu-server-dev/swift/essex diff -Nru swift-1.4.8/debian/patches/CVE-2014-7960.patch swift-1.4.8/debian/patches/CVE-2014-7960.patch --- swift-1.4.8/debian/patches/CVE-2014-7960.patch 1970-01-01 00:00:00.000000000 +0000 +++ swift-1.4.8/debian/patches/CVE-2014-7960.patch 2015-07-27 16:11:29.000000000 +0000 @@ -0,0 +1,401 @@ +Backport of: + +From 2c4622a28ea04e1c6b2382189b0a1f6cccdc9c0f Mon Sep 17 00:00:00 2001 +From: "Richard (Rick) Hawkins" +Date: Wed, 1 Oct 2014 09:37:47 -0400 +Subject: [PATCH] Fix metadata overall limits bug + +Currently metadata limits are checked on a per request basis. If +multiple requests are sent within the per request limits, it is +possible to exceed the overall limits. This patch adds an overall +metadata check to ensure that multiple requests to add metadata to +an account/container will check overall limits before adding +the additional metadata. + +This is a backport to the stable/icehouse branch for commit SHA +5b2c27a5874c2b5b0a333e4955b03544f6a8119f. + +Closes-Bug: 1365350 + +Conflicts: + swift/common/db.py + swift/container/server.py + +Change-Id: Id9fca209c9c1216f1949de7108bbe332808f1045 +--- + swift/account/server.py | 4 +- + swift/common/constraints.py | 5 ++- + swift/common/db.py | 34 ++++++++++++++- + swift/container/server.py | 4 +- + test/functional/test_account.py | 66 ++++++++++++++++++++++++++++ + test/functional/test_container.py | 20 +++++++++ + test/unit/common/test_db.py | 90 ++++++++++++++++++++++++++++++++++++++- + 7 files changed, 216 insertions(+), 7 deletions(-) + +Index: swift-1.4.8/swift/account/server.py +=================================================================== +--- swift-1.4.8.orig/swift/account/server.py ++++ swift-1.4.8/swift/account/server.py +@@ -125,7 +125,7 @@ class AccountController(object): + for key, value in req.headers.iteritems() + if key.lower().startswith('x-account-meta-')) + if metadata: +- broker.update_metadata(metadata) ++ broker.update_metadata(metadata, validate_metadata=True) + if created: + return HTTPCreated(request=req) + else: +@@ -302,7 +302,7 @@ class AccountController(object): + for key, value in req.headers.iteritems() + if key.lower().startswith('x-account-meta-')) + if metadata: +- broker.update_metadata(metadata) ++ broker.update_metadata(metadata, validate_metadata=True) + return HTTPNoContent(request=req) + + def __call__(self, env, start_response): +Index: swift-1.4.8/swift/common/constraints.py +=================================================================== +--- swift-1.4.8.orig/swift/common/constraints.py ++++ swift-1.4.8/swift/common/constraints.py +@@ -41,7 +41,10 @@ MAX_CONTAINER_NAME_LENGTH = 256 + + def check_metadata(req, target_type): + """ +- Check metadata sent in the request headers. ++ Check metadata sent in the request headers. This should only check ++ that the metadata in the request given is valid. Checks against ++ account/container overall metadata should be forwarded on to its ++ respective server to be checked. + + :param req: request object + :param target_type: str: one of: object, container, or account: indicates +Index: swift-1.4.8/swift/common/db.py +=================================================================== +--- swift-1.4.8.orig/swift/common/db.py ++++ swift-1.4.8/swift/common/db.py +@@ -34,7 +34,9 @@ import sqlite3 + + from swift.common.utils import normalize_timestamp, renamer, \ + mkdirs, lock_parent_directory, fallocate ++from swift.common.constraints import MAX_META_COUNT, MAX_META_OVERALL_SIZE + from swift.common.exceptions import LockTimeout ++from webob.exc import HTTPBadRequest + + + #: Timeout for trying to connect to a DB +@@ -551,7 +553,35 @@ class DatabaseBroker(object): + metadata = {} + return metadata + +- def update_metadata(self, metadata_updates): ++ @staticmethod ++ def validate_metadata(metadata): ++ """ ++ Validates that metadata_falls within acceptable limits. ++ ++ :param metadata: to be validated ++ :raises: HTTPBadRequest if MAX_META_COUNT or MAX_META_OVERALL_SIZE ++ is exceeded ++ """ ++ meta_count = 0 ++ meta_size = 0 ++ for key, (value, timestamp) in metadata.iteritems(): ++ key = key.lower() ++ if value != '' and (key.startswith('x-account-meta') or ++ key.startswith('x-container-meta')): ++ prefix = 'x-account-meta-' ++ if key.startswith('x-container-meta-'): ++ prefix = 'x-container-meta-' ++ key = key[len(prefix):] ++ meta_count = meta_count + 1 ++ meta_size = meta_size + len(key) + len(value) ++ if meta_count > MAX_META_COUNT: ++ raise HTTPBadRequest('Too many metadata items; max %d' ++ % MAX_META_COUNT) ++ if meta_size > MAX_META_OVERALL_SIZE: ++ raise HTTPBadRequest('Total metadata too large; max %d' ++ % MAX_META_OVERALL_SIZE) ++ ++ def update_metadata(self, metadata_updates, validate_metadata=False): + """ + Updates the metadata dict for the database. The metadata dict values + are tuples of (value, timestamp) where the timestamp indicates when +@@ -583,6 +613,8 @@ class DatabaseBroker(object): + value, timestamp = value_timestamp + if key not in md or timestamp > md[key][1]: + md[key] = value_timestamp ++ if validate_metadata: ++ DatabaseBroker.validate_metadata(md) + conn.execute('UPDATE %s_stat SET metadata = ?' % self.db_type, + (json.dumps(md),)) + conn.commit() +Index: swift-1.4.8/swift/container/server.py +=================================================================== +--- swift-1.4.8.orig/swift/container/server.py ++++ swift-1.4.8/swift/container/server.py +@@ -221,7 +221,7 @@ class ContainerController(object): + metadata['X-Container-Sync-To'][0] != \ + broker.metadata['X-Container-Sync-To'][0]: + broker.set_x_container_sync_points(-1, -1) +- broker.update_metadata(metadata) ++ broker.update_metadata(metadata, validate_metadata=True) + resp = self.account_update(req, account, container, broker) + if resp: + return resp +@@ -424,7 +424,7 @@ class ContainerController(object): + metadata['X-Container-Sync-To'][0] != \ + broker.metadata['X-Container-Sync-To'][0]: + broker.set_x_container_sync_points(-1, -1) +- broker.update_metadata(metadata) ++ broker.update_metadata(metadata, validate_metadata=True) + return HTTPNoContent(request=req) + + def __call__(self, env, start_response): +Index: swift-1.4.8/test/unit/common/test_db.py +=================================================================== +--- swift-1.4.8.orig/test/unit/common/test_db.py ++++ swift-1.4.8/test/unit/common/test_db.py +@@ -26,12 +26,16 @@ from uuid import uuid4 + + import simplejson + import sqlite3 ++from mock import patch + + import swift.common.db ++from swift.common.constraints import \ ++ MAX_META_VALUE_LENGTH, MAX_META_COUNT, MAX_META_OVERALL_SIZE + from swift.common.db import AccountBroker, chexor, ContainerBroker, \ + DatabaseBroker, DatabaseConnectionError, dict_factory, get_db_connection + from swift.common.utils import normalize_timestamp + from swift.common.exceptions import LockTimeout ++from webob.exc import HTTPException, HTTPBadRequest + + + class TestDatabaseConnectionError(unittest.TestCase): +@@ -145,7 +149,7 @@ class TestDatabaseBroker(unittest.TestCa + conn.execute('CREATE TABLE test (one TEXT)') + conn.execute('CREATE TABLE test_stat (id TEXT)') + conn.execute('INSERT INTO test_stat (id) VALUES (?)', +- (str(uuid4),)) ++ (str(uuid4),)) + conn.execute('INSERT INTO test (one) VALUES ("1")') + conn.commit() + stub_called = [False] +@@ -579,6 +583,97 @@ class TestDatabaseBroker(unittest.TestCa + [first_value, first_timestamp]) + self.assert_('Second' not in broker.metadata) + ++ @patch.object(DatabaseBroker, 'validate_metadata') ++ def test_validate_metadata_is_called_from_update_metadata(self, mock): ++ broker = self.get_replication_info_tester(metadata=True) ++ first_timestamp = normalize_timestamp(1) ++ first_value = '1' ++ metadata = {'First': [first_value, first_timestamp]} ++ broker.update_metadata(metadata, validate_metadata=True) ++ self.assertTrue(mock.called) ++ ++ @patch.object(DatabaseBroker, 'validate_metadata') ++ def test_validate_metadata_is_not_called_from_update_metadata(self, mock): ++ broker = self.get_replication_info_tester(metadata=True) ++ first_timestamp = normalize_timestamp(1) ++ first_value = '1' ++ metadata = {'First': [first_value, first_timestamp]} ++ broker.update_metadata(metadata) ++ self.assertFalse(mock.called) ++ ++ def test_metadata_with_max_count(self): ++ metadata = {} ++ for c in xrange(MAX_META_COUNT): ++ key = 'X-Account-Meta-F{0}'.format(c) ++ metadata[key] = ('B', normalize_timestamp(1)) ++ key = 'X-Account-Meta-Foo'.format(c) ++ metadata[key] = ('', normalize_timestamp(1)) ++ try: ++ DatabaseBroker.validate_metadata(metadata) ++ except HTTPException: ++ self.fail('Unexpected HTTPException') ++ ++ def test_metadata_raises_exception_over_max_count(self): ++ metadata = {} ++ for c in xrange(MAX_META_COUNT + 1): ++ key = 'X-Account-Meta-F{0}'.format(c) ++ metadata[key] = ('B', normalize_timestamp(1)) ++ message = '' ++ try: ++ DatabaseBroker.validate_metadata(metadata) ++ except HTTPBadRequest as e: ++ message = str(e) ++ except HTTPException: ++ self.fail('Unexpected HTTPException') ++ self.assertEqual(message, 'Too many metadata items; max %d' % ++ MAX_META_COUNT) ++ ++ def test_metadata_with_max_overall_size(self): ++ metadata = {} ++ metadata_value = 'v' * MAX_META_VALUE_LENGTH ++ size = 0 ++ x = 0 ++ while size < (MAX_META_OVERALL_SIZE - 4 ++ - MAX_META_VALUE_LENGTH): ++ size += 4 + MAX_META_VALUE_LENGTH ++ metadata['X-Account-Meta-%04d' % x] = (metadata_value, ++ normalize_timestamp(1)) ++ x += 1 ++ if MAX_META_OVERALL_SIZE - size > 1: ++ metadata['X-Account-Meta-k'] = ( ++ 'v' * (MAX_META_OVERALL_SIZE - size - 1), ++ normalize_timestamp(1)) ++ try: ++ DatabaseBroker.validate_metadata(metadata) ++ except HTTPException: ++ self.fail('Unexpected HTTPException') ++ ++ def test_metadata_raises_exception_over_max_overall_size(self): ++ metadata = {} ++ metadata_value = 'k' * MAX_META_VALUE_LENGTH ++ size = 0 ++ x = 0 ++ while size < (MAX_META_OVERALL_SIZE - 4 ++ - MAX_META_VALUE_LENGTH): ++ size += 4 + MAX_META_VALUE_LENGTH ++ metadata['X-Account-Meta-%04d' % x] = (metadata_value, ++ normalize_timestamp(1)) ++ x += 1 ++ if MAX_META_OVERALL_SIZE - size > 1: ++ metadata['X-Account-Meta-k'] = ( ++ 'v' * (MAX_META_OVERALL_SIZE - size - 1), ++ normalize_timestamp(1)) ++ metadata['X-Account-Meta-k2'] = ('v', normalize_timestamp(1)) ++ message = '' ++ try: ++ DatabaseBroker.validate_metadata(metadata) ++ except HTTPBadRequest as e: ++ message = str(e) ++ except HTTPException: ++ self.fail('Unexpected HTTPException') ++ self.assertEqual(message, 'Total metadata too large; max %d' % ++ MAX_META_OVERALL_SIZE) ++ + + class TestContainerBroker(unittest.TestCase): + """ Tests for swift.common.db.ContainerBroker """ +Index: swift-1.4.8/test/functionalnosetests/test_account.py +=================================================================== +--- swift-1.4.8.orig/test/functionalnosetests/test_account.py ++++ swift-1.4.8/test/functionalnosetests/test_account.py +@@ -11,6 +11,42 @@ from swift_testing import check_response + + class TestAccount(unittest.TestCase): + ++ def setUp(self): ++ if skip: ++ return ++ def head(url, token, parsed, conn): ++ conn.request('HEAD', parsed.path, '', {'X-Auth-Token': token}) ++ return check_response(conn) ++ resp = retry(head) ++ resp.read() ++ self.existing_metadata = set([ ++ k for k, v in resp.getheaders() if ++ k.lower().startswith('x-account-meta')]) ++ ++ def tearDown(self): ++ if skip: ++ return ++ def head(url, token, parsed, conn): ++ conn.request('HEAD', parsed.path, '', {'X-Auth-Token': token}) ++ return check_response(conn) ++ resp = retry(head) ++ resp.read() ++ new_metadata = set( ++ [k for k, v in resp.getheaders() if ++ k.lower().startswith('x-account-meta')]) ++ ++ def clear_meta(url, token, parsed, conn, remove_metadata_keys): ++ headers = {'X-Auth-Token': token} ++ headers.update((k, '') for k in remove_metadata_keys) ++ conn.request('POST', parsed.path, '', headers) ++ return check_response(conn) ++ extra_metadata = list(self.existing_metadata ^ new_metadata) ++ for i in range(0, len(extra_metadata), 90): ++ batch = extra_metadata[i:i + 90] ++ resp = retry(clear_meta, batch) ++ resp.read() ++ self.assertEqual(resp.status // 100, 2) ++ + def test_metadata(self): + if skip: + raise SkipTest +@@ -99,6 +135,16 @@ class TestAccount(unittest.TestCase): + resp.read() + self.assertEquals(resp.status, 400) + ++ def test_bad_metadata2(self): ++ if skip: ++ raise SkipTest ++ ++ def post(url, token, parsed, conn, extra_headers): ++ headers = {'X-Auth-Token': token} ++ headers.update(extra_headers) ++ conn.request('POST', parsed.path, '', headers) ++ return check_response(conn) ++ + headers = {} + for x in xrange(MAX_META_COUNT): + headers['X-Account-Meta-%d' % x] = 'v' +@@ -112,6 +158,16 @@ class TestAccount(unittest.TestCase): + resp.read() + self.assertEquals(resp.status, 400) + ++ def test_bad_metadata3(self): ++ if skip: ++ raise SkipTest ++ ++ def post(url, token, parsed, conn, extra_headers): ++ headers = {'X-Auth-Token': token} ++ headers.update(extra_headers) ++ conn.request('POST', parsed.path, '', headers) ++ return check_response(conn) ++ + headers = {} + header_value = 'k' * MAX_META_VALUE_LENGTH + size = 0 +Index: swift-1.4.8/test/functionalnosetests/test_container.py +=================================================================== +--- swift-1.4.8.orig/test/functionalnosetests/test_container.py ++++ swift-1.4.8/test/functionalnosetests/test_container.py +@@ -291,6 +291,16 @@ class TestContainer(unittest.TestCase): + resp.read() + self.assertEquals(resp.status, 400) + ++ def test_POST_bad_metadata2(self): ++ if skip: ++ raise SkipTest ++ ++ def post(url, token, parsed, conn, extra_headers): ++ headers = {'X-Auth-Token': token} ++ headers.update(extra_headers) ++ conn.request('POST', parsed.path + '/' + self.name, '', headers) ++ return check_response(conn) ++ + headers = {} + for x in xrange(MAX_META_COUNT): + headers['X-Container-Meta-%d' % x] = 'v' +@@ -304,6 +314,16 @@ class TestContainer(unittest.TestCase): + resp.read() + self.assertEquals(resp.status, 400) + ++ def test_POST_bad_metadata3(self): ++ if skip: ++ raise SkipTest ++ ++ def post(url, token, parsed, conn, extra_headers): ++ headers = {'X-Auth-Token': token} ++ headers.update(extra_headers) ++ conn.request('POST', parsed.path + '/' + self.name, '', headers) ++ return check_response(conn) ++ + headers = {} + header_value = 'k' * MAX_META_VALUE_LENGTH + size = 0 diff -Nru swift-1.4.8/debian/patches/series swift-1.4.8/debian/patches/series --- swift-1.4.8/debian/patches/series 2014-03-14 18:21:24.000000000 +0000 +++ swift-1.4.8/debian/patches/series 2015-07-22 19:17:20.000000000 +0000 @@ -5,3 +5,4 @@ memcache_serialization_support-default-to-zero.patch CVE-2013-4155.patch CVE-2014-0006.patch +CVE-2014-7960.patch