components/openstack/swift/patches/CVE-2014-7960.patch
changeset 3998 5bd484384122
parent 3997 0ca3f3d6c919
child 4002 95b8f35fcdd5
equal deleted inserted replaced
3997:0ca3f3d6c919 3998:5bd484384122
     1 This is a backport to Havana of the Icehouse backport of the fix to bug 1365350,
       
     2 plus a fix to the testsuite to actually test the problem (bug 1381159).  Because
       
     3 Havana is out of support, it is not a candidate for upstream submission,
       
     4 though a re-work of the testsuite changes to trunk Swift is.
       
     5 
       
     6 ================================================================================
       
     7 
       
     8 From 2c4622a28ea04e1c6b2382189b0a1f6cccdc9c0f Mon Sep 17 00:00:00 2001
       
     9 From: "Richard (Rick) Hawkins" <[email protected]>
       
    10 Date: Wed, 1 Oct 2014 09:37:47 -0400
       
    11 Subject: [PATCH] Fix metadata overall limits bug
       
    12 
       
    13 Currently metadata limits are checked on a per request basis. If
       
    14 multiple requests are sent within the per request limits, it is
       
    15 possible to exceed the overall limits.  This patch adds an overall
       
    16 metadata check to ensure that multiple requests to add metadata to
       
    17 an account/container will check overall limits before adding
       
    18 the additional metadata.
       
    19 
       
    20 This is a backport to the stable/icehouse branch for commit SHA
       
    21 5b2c27a5874c2b5b0a333e4955b03544f6a8119f.
       
    22 
       
    23 Closes-Bug: 1365350
       
    24 
       
    25 Conflicts:
       
    26 	swift/common/db.py
       
    27 	swift/container/server.py
       
    28 
       
    29 Change-Id: Id9fca209c9c1216f1949de7108bbe332808f1045
       
    30 ---
       
    31  swift/account/server.py           |    4 +-
       
    32  swift/common/constraints.py       |    5 ++-
       
    33  swift/common/db.py                |   34 +++++++++++++-
       
    34  swift/container/server.py         |    4 +-
       
    35  test/functional/test_account.py   |   66 +++++++++++++++++++++++++++
       
    36  test/functional/test_container.py |   20 +++++++++
       
    37  test/unit/common/test_db.py       |   90 ++++++++++++++++++++++++++++++++++++-
       
    38  7 files changed, 216 insertions(+), 7 deletions(-)
       
    39 
       
    40 --- a/swift/account/server.py
       
    41 +++ b/swift/account/server.py
       
    42 @@ -156,7 +156,7 @@ class AccountController(object):
       
    43                              for key, value in req.headers.iteritems()
       
    44                              if key.lower().startswith('x-account-meta-'))
       
    45              if metadata:
       
    46 -                broker.update_metadata(metadata)
       
    47 +                broker.update_metadata(metadata, validate_metadata=True)
       
    48              if created:
       
    49                  return HTTPCreated(request=req)
       
    50              else:
       
    51 @@ -262,7 +262,7 @@ class AccountController(object):
       
    52                          for key, value in req.headers.iteritems()
       
    53                          if key.lower().startswith('x-account-meta-'))
       
    54          if metadata:
       
    55 -            broker.update_metadata(metadata)
       
    56 +            broker.update_metadata(metadata, validate_metadata=True)
       
    57          return HTTPNoContent(request=req)
       
    58  
       
    59      def __call__(self, env, start_response):
       
    60 --- a/swift/common/constraints.py
       
    61 +++ b/swift/common/constraints.py
       
    62 @@ -68,7 +68,10 @@ FORMAT2CONTENT_TYPE = {'plain': 'text/pl
       
    63  
       
    64  def check_metadata(req, target_type):
       
    65      """
       
    66 -    Check metadata sent in the request headers.
       
    67 +    Check metadata sent in the request headers.  This should only check
       
    68 +    that the metadata in the request given is valid.  Checks against
       
    69 +    account/container overall metadata should be forwarded on to its
       
    70 +    respective server to be checked.
       
    71  
       
    72      :param req: request object
       
    73      :param target_type: str: one of: object, container, or account: indicates
       
    74 --- a/swift/common/db.py
       
    75 +++ b/swift/common/db.py
       
    76 @@ -32,7 +32,9 @@ import sqlite3
       
    77  
       
    78  from swift.common.utils import json, normalize_timestamp, renamer, \
       
    79      mkdirs, lock_parent_directory, fallocate
       
    80 +from swift.common.constraints import MAX_META_COUNT, MAX_META_OVERALL_SIZE
       
    81  from swift.common.exceptions import LockTimeout
       
    82 +from swift.common.swob import HTTPBadRequest
       
    83  
       
    84  
       
    85  #: Whether calls will be made to preallocate disk space for database files.
       
    86 @@ -615,7 +617,35 @@ class DatabaseBroker(object):
       
    87              metadata = {}
       
    88          return metadata
       
    89  
       
    90 -    def update_metadata(self, metadata_updates):
       
    91 +    @staticmethod
       
    92 +    def validate_metadata(metadata):
       
    93 +        """
       
    94 +        Validates that metadata_falls within acceptable limits.
       
    95 +
       
    96 +        :param metadata: to be validated
       
    97 +        :raises: HTTPBadRequest if MAX_META_COUNT or MAX_META_OVERALL_SIZE
       
    98 +                 is exceeded
       
    99 +        """
       
   100 +        meta_count = 0
       
   101 +        meta_size = 0
       
   102 +        for key, (value, timestamp) in metadata.iteritems():
       
   103 +            key = key.lower()
       
   104 +            if value != '' and (key.startswith('x-account-meta') or
       
   105 +                                key.startswith('x-container-meta')):
       
   106 +                prefix = 'x-account-meta-'
       
   107 +                if key.startswith('x-container-meta-'):
       
   108 +                    prefix = 'x-container-meta-'
       
   109 +                key = key[len(prefix):]
       
   110 +                meta_count = meta_count + 1
       
   111 +                meta_size = meta_size + len(key) + len(value)
       
   112 +        if meta_count > MAX_META_COUNT:
       
   113 +            raise HTTPBadRequest('Too many metadata items; max %d'
       
   114 +                                 % MAX_META_COUNT)
       
   115 +        if meta_size > MAX_META_OVERALL_SIZE:
       
   116 +            raise HTTPBadRequest('Total metadata too large; max %d'
       
   117 +                                 % MAX_META_OVERALL_SIZE)
       
   118 +
       
   119 +    def update_metadata(self, metadata_updates, validate_metadata=False):
       
   120          """
       
   121          Updates the metadata dict for the database. The metadata dict values
       
   122          are tuples of (value, timestamp) where the timestamp indicates when
       
   123 @@ -648,6 +678,8 @@ class DatabaseBroker(object):
       
   124                  value, timestamp = value_timestamp
       
   125                  if key not in md or timestamp > md[key][1]:
       
   126                      md[key] = value_timestamp
       
   127 +            if validate_metadata:
       
   128 +                DatabaseBroker.validate_metadata(md)
       
   129              conn.execute('UPDATE %s_stat SET metadata = ?' % self.db_type,
       
   130                           (json.dumps(md),))
       
   131              conn.commit()
       
   132 --- a/swift/container/server.py
       
   133 +++ b/swift/container/server.py
       
   134 @@ -275,7 +275,7 @@ class ContainerController(object):
       
   135                              metadata['X-Container-Sync-To'][0] != \
       
   136                              broker.metadata['X-Container-Sync-To'][0]:
       
   137                          broker.set_x_container_sync_points(-1, -1)
       
   138 -                broker.update_metadata(metadata)
       
   139 +                broker.update_metadata(metadata, validate_metadata=True)
       
   140              resp = self.account_update(req, account, container, broker)
       
   141              if resp:
       
   142                  return resp
       
   143 @@ -461,7 +461,7 @@ class ContainerController(object):
       
   144                          metadata['X-Container-Sync-To'][0] != \
       
   145                          broker.metadata['X-Container-Sync-To'][0]:
       
   146                      broker.set_x_container_sync_points(-1, -1)
       
   147 -            broker.update_metadata(metadata)
       
   148 +            broker.update_metadata(metadata, validate_metadata=True)
       
   149          return HTTPNoContent(request=req)
       
   150  
       
   151      def __call__(self, env, start_response):
       
   152 --- a/test/functional/swift_testing.py
       
   153 +++ b/test/functional/swift_testing.py
       
   154 @@ -0,0 +1,231 @@
       
   155 +# Copyright (c) 2010-2012 OpenStack Foundation
       
   156 +#
       
   157 +# Licensed under the Apache License, Version 2.0 (the "License");
       
   158 +# you may not use this file except in compliance with the License.
       
   159 +# You may obtain a copy of the License at
       
   160 +#
       
   161 +#    http://www.apache.org/licenses/LICENSE-2.0
       
   162 +#
       
   163 +# Unless required by applicable law or agreed to in writing, software
       
   164 +# distributed under the License is distributed on an "AS IS" BASIS,
       
   165 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
       
   166 +# implied.
       
   167 +# See the License for the specific language governing permissions and
       
   168 +# limitations under the License.
       
   169 +
       
   170 +from httplib import HTTPException
       
   171 +import os
       
   172 +import socket
       
   173 +import sys
       
   174 +from time import sleep
       
   175 +from urlparse import urlparse
       
   176 +import functools
       
   177 +from nose import SkipTest
       
   178 +
       
   179 +from test import get_config
       
   180 +
       
   181 +from swiftclient import get_auth, http_connection
       
   182 +from test.functional.swift_test_client import Connection
       
   183 +
       
   184 +conf = get_config('func_test')
       
   185 +web_front_end = conf.get('web_front_end', 'integral')
       
   186 +normalized_urls = conf.get('normalized_urls', False)
       
   187 +
       
   188 +# If no conf was read, we will fall back to old school env vars
       
   189 +swift_test_auth = os.environ.get('SWIFT_TEST_AUTH')
       
   190 +swift_test_user = [os.environ.get('SWIFT_TEST_USER'), None, None]
       
   191 +swift_test_key = [os.environ.get('SWIFT_TEST_KEY'), None, None]
       
   192 +swift_test_tenant = ['', '', '']
       
   193 +swift_test_perm = ['', '', '']
       
   194 +
       
   195 +if conf:
       
   196 +    swift_test_auth_version = str(conf.get('auth_version', '1'))
       
   197 +
       
   198 +    swift_test_auth = 'http'
       
   199 +    if conf.get('auth_ssl', 'no').lower() in ('yes', 'true', 'on', '1'):
       
   200 +        swift_test_auth = 'https'
       
   201 +    if 'auth_prefix' not in conf:
       
   202 +        conf['auth_prefix'] = '/'
       
   203 +    try:
       
   204 +        suffix = '://%(auth_host)s:%(auth_port)s%(auth_prefix)s' % conf
       
   205 +        swift_test_auth += suffix
       
   206 +    except KeyError:
       
   207 +        pass  # skip
       
   208 +
       
   209 +    if swift_test_auth_version == "1":
       
   210 +        swift_test_auth += 'v1.0'
       
   211 +
       
   212 +        if 'account' in conf:
       
   213 +            swift_test_user[0] = '%(account)s:%(username)s' % conf
       
   214 +        else:
       
   215 +            swift_test_user[0] = '%(username)s' % conf
       
   216 +        swift_test_key[0] = conf['password']
       
   217 +        try:
       
   218 +            swift_test_user[1] = '%s%s' % (
       
   219 +                '%s:' % conf['account2'] if 'account2' in conf else '',
       
   220 +                conf['username2'])
       
   221 +            swift_test_key[1] = conf['password2']
       
   222 +        except KeyError as err:
       
   223 +            pass  # old conf, no second account tests can be run
       
   224 +        try:
       
   225 +            swift_test_user[2] = '%s%s' % ('%s:' % conf['account'] if 'account'
       
   226 +                                           in conf else '', conf['username3'])
       
   227 +            swift_test_key[2] = conf['password3']
       
   228 +        except KeyError as err:
       
   229 +            pass  # old conf, no third account tests can be run
       
   230 +
       
   231 +        for _ in range(3):
       
   232 +            swift_test_perm[_] = swift_test_user[_]
       
   233 +
       
   234 +    else:
       
   235 +        swift_test_user[0] = conf['username']
       
   236 +        swift_test_tenant[0] = conf['account']
       
   237 +        swift_test_key[0] = conf['password']
       
   238 +        swift_test_user[1] = conf['username2']
       
   239 +        swift_test_tenant[1] = conf['account2']
       
   240 +        swift_test_key[1] = conf['password2']
       
   241 +        swift_test_user[2] = conf['username3']
       
   242 +        swift_test_tenant[2] = conf['account']
       
   243 +        swift_test_key[2] = conf['password3']
       
   244 +
       
   245 +        for _ in range(3):
       
   246 +            swift_test_perm[_] = swift_test_tenant[_] + ':' \
       
   247 +                + swift_test_user[_]
       
   248 +
       
   249 +skip = not all([swift_test_auth, swift_test_user[0], swift_test_key[0]])
       
   250 +if skip:
       
   251 +    print >>sys.stderr, 'SKIPPING FUNCTIONAL TESTS DUE TO NO CONFIG'
       
   252 +
       
   253 +skip2 = not all([not skip, swift_test_user[1], swift_test_key[1]])
       
   254 +if not skip and skip2:
       
   255 +    print >>sys.stderr, \
       
   256 +        'SKIPPING SECOND ACCOUNT FUNCTIONAL TESTS DUE TO NO CONFIG FOR THEM'
       
   257 +
       
   258 +skip3 = not all([not skip, swift_test_user[2], swift_test_key[2]])
       
   259 +if not skip and skip3:
       
   260 +    print >>sys.stderr, \
       
   261 +        'SKIPPING THIRD ACCOUNT FUNCTIONAL TESTS DUE TO NO CONFIG FOR THEM'
       
   262 +
       
   263 +
       
   264 +class AuthError(Exception):
       
   265 +    pass
       
   266 +
       
   267 +
       
   268 +class InternalServerError(Exception):
       
   269 +    pass
       
   270 +
       
   271 +
       
   272 +url = [None, None, None]
       
   273 +token = [None, None, None]
       
   274 +parsed = [None, None, None]
       
   275 +conn = [None, None, None]
       
   276 +
       
   277 +
       
   278 +def retry(func, *args, **kwargs):
       
   279 +    """
       
   280 +    You can use the kwargs to override:
       
   281 +      'retries' (default: 5)
       
   282 +      'use_account' (default: 1) - which user's token to pass
       
   283 +      'url_account' (default: matches 'use_account') - which user's storage URL
       
   284 +      'resource' (default: url[url_account] - URL to connect to; retry()
       
   285 +          will interpolate the variable :storage_url: if present
       
   286 +    """
       
   287 +    global url, token, parsed, conn
       
   288 +    retries = kwargs.get('retries', 5)
       
   289 +    attempts, backoff = 0, 1
       
   290 +
       
   291 +    # use account #1 by default; turn user's 1-indexed account into 0-indexed
       
   292 +    use_account = kwargs.pop('use_account', 1) - 1
       
   293 +
       
   294 +    # access our own account by default
       
   295 +    url_account = kwargs.pop('url_account', use_account + 1) - 1
       
   296 +
       
   297 +    while attempts <= retries:
       
   298 +        attempts += 1
       
   299 +        try:
       
   300 +            if not url[use_account] or not token[use_account]:
       
   301 +                url[use_account], token[use_account] = \
       
   302 +                    get_auth(swift_test_auth, swift_test_user[use_account],
       
   303 +                             swift_test_key[use_account],
       
   304 +                             snet=False,
       
   305 +                             tenant_name=swift_test_tenant[use_account],
       
   306 +                             auth_version=swift_test_auth_version,
       
   307 +                             os_options={})
       
   308 +                parsed[use_account] = conn[use_account] = None
       
   309 +            if not parsed[use_account] or not conn[use_account]:
       
   310 +                parsed[use_account], conn[use_account] = \
       
   311 +                    http_connection(url[use_account])
       
   312 +
       
   313 +            # default resource is the account url[url_account]
       
   314 +            resource = kwargs.pop('resource', '%(storage_url)s')
       
   315 +            template_vars = {'storage_url': url[url_account]}
       
   316 +            parsed_result = urlparse(resource % template_vars)
       
   317 +            return func(url[url_account], token[use_account],
       
   318 +                        parsed_result, conn[url_account],
       
   319 +                        *args, **kwargs)
       
   320 +        except (socket.error, HTTPException):
       
   321 +            if attempts > retries:
       
   322 +                raise
       
   323 +            parsed[use_account] = conn[use_account] = None
       
   324 +        except AuthError:
       
   325 +            url[use_account] = token[use_account] = None
       
   326 +            continue
       
   327 +        except InternalServerError:
       
   328 +            pass
       
   329 +        if attempts <= retries:
       
   330 +            sleep(backoff)
       
   331 +            backoff *= 2
       
   332 +    raise Exception('No result after %s retries.' % retries)
       
   333 +
       
   334 +
       
   335 +def check_response(conn):
       
   336 +    resp = conn.getresponse()
       
   337 +    if resp.status == 401:
       
   338 +        resp.read()
       
   339 +        raise AuthError()
       
   340 +    elif resp.status // 100 == 5:
       
   341 +        resp.read()
       
   342 +        raise InternalServerError()
       
   343 +    return resp
       
   344 +
       
   345 +cluster_info = {}
       
   346 +
       
   347 +
       
   348 +def get_cluster_info():
       
   349 +    conn = Connection(conf)
       
   350 +    conn.authenticate()
       
   351 +    global cluster_info
       
   352 +    cluster_info = conn.cluster_info()
       
   353 +
       
   354 +
       
   355 +def reset_acl():
       
   356 +    def post(url, token, parsed, conn):
       
   357 +        conn.request('POST', parsed.path, '', {
       
   358 +            'X-Auth-Token': token,
       
   359 +            'X-Account-Access-Control': '{}'
       
   360 +        })
       
   361 +        return check_response(conn)
       
   362 +    resp = retry(post, use_account=1)
       
   363 +    resp.read()
       
   364 +
       
   365 +
       
   366 +def requires_acls(f):
       
   367 +    @functools.wraps(f)
       
   368 +    def wrapper(*args, **kwargs):
       
   369 +        if skip:
       
   370 +            raise SkipTest
       
   371 +        if not cluster_info:
       
   372 +            get_cluster_info()
       
   373 +        # Determine whether this cluster has account ACLs; if not, skip test
       
   374 +        if not cluster_info.get('tempauth', {}).get('account_acls'):
       
   375 +            raise SkipTest
       
   376 +        if 'keystoneauth' in cluster_info:
       
   377 +            # remove when keystoneauth supports account acls
       
   378 +            raise SkipTest
       
   379 +        reset_acl()
       
   380 +        try:
       
   381 +            rv = f(*args, **kwargs)
       
   382 +        finally:
       
   383 +            reset_acl()
       
   384 +        return rv
       
   385 +    return wrapper
       
   386 --- a/test/functional/tests.py
       
   387 +++ b/test/functional/tests.py
       
   388 @@ -1624,5 +1624,466 @@ class TestFileComparison(Base):
       
   389  class TestFileComparisonUTF8(Base2, TestFileComparison):
       
   390      set_up = False
       
   391  
       
   392 +import json
       
   393 +from uuid import uuid4
       
   394 +from swift_testing import (check_response, retry, skip,
       
   395 +                           web_front_end, requires_acls)
       
   396 +import swift_testing
       
   397 +
       
   398 +class TestCVE20147960Container(unittest.TestCase):
       
   399 +
       
   400 +    def setUp(self):
       
   401 +        if skip:
       
   402 +            raise SkipTest
       
   403 +        self.name = uuid4().hex
       
   404 +        # this container isn't created by default, but will be cleaned up
       
   405 +        self.container = uuid4().hex
       
   406 +
       
   407 +        def put(url, token, parsed, conn):
       
   408 +            conn.request('PUT', parsed.path + '/' + self.name, '',
       
   409 +                         {'X-Auth-Token': token})
       
   410 +            return check_response(conn)
       
   411 +
       
   412 +        resp = retry(put)
       
   413 +        resp.read()
       
   414 +        self.assertEqual(resp.status, 201)
       
   415 +
       
   416 +        self.max_meta_count = load_constraint('max_meta_count')
       
   417 +        self.max_meta_name_length = load_constraint('max_meta_name_length')
       
   418 +        self.max_meta_overall_size = load_constraint('max_meta_overall_size')
       
   419 +        self.max_meta_value_length = load_constraint('max_meta_value_length')
       
   420 +
       
   421 +    def tearDown(self):
       
   422 +        if skip:
       
   423 +            raise SkipTest
       
   424 +
       
   425 +        def get(url, token, parsed, conn, container):
       
   426 +            conn.request(
       
   427 +                'GET', parsed.path + '/' + container + '?format=json', '',
       
   428 +                {'X-Auth-Token': token})
       
   429 +            return check_response(conn)
       
   430 +
       
   431 +        def delete(url, token, parsed, conn, container, obj):
       
   432 +            conn.request(
       
   433 +                'DELETE', '/'.join([parsed.path, container, obj['name']]), '',
       
   434 +                {'X-Auth-Token': token})
       
   435 +            return check_response(conn)
       
   436 +
       
   437 +        for container in (self.name, self.container):
       
   438 +            while True:
       
   439 +                resp = retry(get, container)
       
   440 +                body = resp.read()
       
   441 +                if resp.status == 404:
       
   442 +                    break
       
   443 +                self.assert_(resp.status // 100 == 2, resp.status)
       
   444 +                objs = json.loads(body)
       
   445 +                if not objs:
       
   446 +                    break
       
   447 +                for obj in objs:
       
   448 +                    resp = retry(delete, container, obj)
       
   449 +                    resp.read()
       
   450 +                    self.assertEqual(resp.status, 204)
       
   451 +
       
   452 +        def delete(url, token, parsed, conn, container):
       
   453 +            conn.request('DELETE', parsed.path + '/' + container, '',
       
   454 +                         {'X-Auth-Token': token})
       
   455 +            return check_response(conn)
       
   456 +
       
   457 +        resp = retry(delete, self.name)
       
   458 +        resp.read()
       
   459 +        self.assertEqual(resp.status, 204)
       
   460 +
       
   461 +        # container may have not been created
       
   462 +        resp = retry(delete, self.container)
       
   463 +        resp.read()
       
   464 +        self.assert_(resp.status in (204, 404))
       
   465 +
       
   466 +    def test_POST_bad_metadata(self):
       
   467 +        if skip:
       
   468 +            raise SkipTest
       
   469 +
       
   470 +        def post(url, token, parsed, conn, extra_headers):
       
   471 +            headers = {'X-Auth-Token': token}
       
   472 +            headers.update(extra_headers)
       
   473 +            conn.request('POST', parsed.path + '/' + self.name, '', headers)
       
   474 +            return check_response(conn)
       
   475 +
       
   476 +        resp = retry(
       
   477 +            post,
       
   478 +            {'X-Container-Meta-' + ('k' * self.max_meta_name_length): 'v'})
       
   479 +        resp.read()
       
   480 +        self.assertEqual(resp.status, 204)
       
   481 +        resp = retry(
       
   482 +            post,
       
   483 +            {'X-Container-Meta-' + (
       
   484 +                'k' * (self.max_meta_name_length + 1)): 'v'})
       
   485 +        resp.read()
       
   486 +        self.assertEqual(resp.status, 400)
       
   487 +
       
   488 +        resp = retry(
       
   489 +            post,
       
   490 +            {'X-Container-Meta-Too-Long': 'k' * self.max_meta_value_length})
       
   491 +        resp.read()
       
   492 +        self.assertEqual(resp.status, 204)
       
   493 +        resp = retry(
       
   494 +            post,
       
   495 +            {'X-Container-Meta-Too-Long': 'k' * (
       
   496 +                self.max_meta_value_length + 1)})
       
   497 +        resp.read()
       
   498 +        self.assertEqual(resp.status, 400)
       
   499 +
       
   500 +    def test_POST_maxcount_metadata(self):
       
   501 +        if skip:
       
   502 +            raise SkipTest
       
   503 +
       
   504 +        def post(url, token, parsed, conn, extra_headers):
       
   505 +            headers = {'X-Auth-Token': token}
       
   506 +            headers.update(extra_headers)
       
   507 +            conn.request('POST', parsed.path + '/' + self.name, '', headers)
       
   508 +            return check_response(conn)
       
   509 +
       
   510 +        headers = {}
       
   511 +        for x in xrange(self.max_meta_count):
       
   512 +            headers['X-Container-Meta-%d' % x] = 'v'
       
   513 +        resp = retry(post, headers)
       
   514 +        resp.read()
       
   515 +        self.assertEqual(resp.status, 204)
       
   516 +
       
   517 +    def test_POST_maxcount_metadata_over_one(self):
       
   518 +        if skip:
       
   519 +            raise SkipTest
       
   520 +
       
   521 +        def post(url, token, parsed, conn, extra_headers):
       
   522 +            headers = {'X-Auth-Token': token}
       
   523 +            headers.update(extra_headers)
       
   524 +            conn.request('POST', parsed.path + '/' + self.name, '', headers)
       
   525 +            return check_response(conn)
       
   526 +
       
   527 +        headers = {}
       
   528 +        for x in xrange(self.max_meta_count + 1):
       
   529 +            headers['X-Container-Meta-%d' % x] = 'v'
       
   530 +        resp = retry(post, headers)
       
   531 +        resp.read()
       
   532 +        self.assertEqual(resp.status, 400)
       
   533 +
       
   534 +    def test_POST_maxcount_metadata_over_two(self):
       
   535 +        if skip:
       
   536 +            raise SkipTest
       
   537 +
       
   538 +        def post(url, token, parsed, conn, extra_headers):
       
   539 +            headers = {'X-Auth-Token': token}
       
   540 +            headers.update(extra_headers)
       
   541 +            conn.request('POST', parsed.path + '/' + self.name, '', headers)
       
   542 +            return check_response(conn)
       
   543 +
       
   544 +        headers = {}
       
   545 +        for x in xrange(self.max_meta_count - 1):
       
   546 +            headers['X-Container-Meta-%d' % x] = 'v'
       
   547 +        resp = retry(post, headers)
       
   548 +        resp.read()
       
   549 +        self.assertEqual(resp.status, 204)
       
   550 +
       
   551 +        headers = {}
       
   552 +        for x in xrange(self.max_meta_count - 1, self.max_meta_count + 1):
       
   553 +            headers['X-Container-Meta-%d' % x] = 'v'
       
   554 +        resp = retry(post, headers)
       
   555 +        resp.read()
       
   556 +        self.assertEqual(resp.status, 400)
       
   557 +
       
   558 +    def test_POST_maxsize_metadata(self):
       
   559 +        if skip:
       
   560 +            raise SkipTest
       
   561 +
       
   562 +        def post(url, token, parsed, conn, extra_headers):
       
   563 +            headers = {'X-Auth-Token': token}
       
   564 +            headers.update(extra_headers)
       
   565 +            conn.request('POST', parsed.path + '/' + self.name, '', headers)
       
   566 +            return check_response(conn)
       
   567 +
       
   568 +        headers = {}
       
   569 +        header_value = 'k' * self.max_meta_value_length
       
   570 +        size = 0
       
   571 +        x = 0
       
   572 +        while size < (self.max_meta_overall_size - 4
       
   573 +                      - self.max_meta_value_length):
       
   574 +            size += 4 + self.max_meta_value_length
       
   575 +            headers['X-Container-Meta-%04d' % x] = header_value
       
   576 +            x += 1
       
   577 +        if self.max_meta_overall_size - size > 1:
       
   578 +            headers['X-Container-Meta-k'] = \
       
   579 +                'v' * (self.max_meta_overall_size - size - 1)
       
   580 +        resp = retry(post, headers)
       
   581 +        resp.read()
       
   582 +        self.assertEqual(resp.status, 204)
       
   583 +
       
   584 +    def test_POST_maxsize_metadata_over_one(self):
       
   585 +        if skip:
       
   586 +            raise SkipTest
       
   587 +
       
   588 +        def post(url, token, parsed, conn, extra_headers):
       
   589 +            headers = {'X-Auth-Token': token}
       
   590 +            headers.update(extra_headers)
       
   591 +            conn.request('POST', parsed.path + '/' + self.name, '', headers)
       
   592 +            return check_response(conn)
       
   593 +
       
   594 +        headers = {}
       
   595 +        header_value = 'k' * self.max_meta_value_length
       
   596 +        size = 0
       
   597 +        x = 0
       
   598 +        while size < (self.max_meta_overall_size - 4
       
   599 +                      - self.max_meta_value_length):
       
   600 +            size += 4 + self.max_meta_value_length
       
   601 +            headers['X-Container-Meta-%04d' % x] = header_value
       
   602 +            x += 1
       
   603 +        if self.max_meta_overall_size >= size:
       
   604 +            headers['X-Container-Meta-k'] = \
       
   605 +                'v' * (self.max_meta_overall_size - size)
       
   606 +        resp = retry(post, headers)
       
   607 +        resp.read()
       
   608 +        self.assertEqual(resp.status, 400)
       
   609 +
       
   610 +    def test_POST_maxsize_metadata_over_two(self):
       
   611 +        if skip:
       
   612 +            raise SkipTest
       
   613 +
       
   614 +        def post(url, token, parsed, conn, extra_headers):
       
   615 +            headers = {'X-Auth-Token': token}
       
   616 +            headers.update(extra_headers)
       
   617 +            conn.request('POST', parsed.path + '/' + self.name, '', headers)
       
   618 +            return check_response(conn)
       
   619 +
       
   620 +        headers = {}
       
   621 +        header_value = 'k' * self.max_meta_value_length
       
   622 +        size = 0
       
   623 +        x = 0
       
   624 +        while size < (self.max_meta_overall_size - 4
       
   625 +                      - self.max_meta_value_length):
       
   626 +            size += 4 + self.max_meta_value_length
       
   627 +            headers['X-Container-Meta-%04d' % x] = header_value
       
   628 +            x += 1
       
   629 +        if self.max_meta_overall_size - size > 1:
       
   630 +            headers['X-Container-Meta-k'] = \
       
   631 +                'v' * (self.max_meta_overall_size - size - 1)
       
   632 +        resp = retry(post, headers)
       
   633 +        resp.read()
       
   634 +        self.assertEqual(resp.status, 204)
       
   635 +        headers = {}
       
   636 +        headers['X-Container-Meta-k2'] = 'v'
       
   637 +        resp = retry(post, headers)
       
   638 +        resp.read()
       
   639 +        self.assertEqual(resp.status, 400)
       
   640 +
       
   641 +
       
   642 +class TestCVE20147960Account(unittest.TestCase):
       
   643 +
       
   644 +    def setUp(self):
       
   645 +        self.max_meta_count = load_constraint('max_meta_count')
       
   646 +        self.max_meta_name_length = load_constraint('max_meta_name_length')
       
   647 +        self.max_meta_overall_size = load_constraint('max_meta_overall_size')
       
   648 +        self.max_meta_value_length = load_constraint('max_meta_value_length')
       
   649 +
       
   650 +        def head(url, token, parsed, conn):
       
   651 +            conn.request('HEAD', parsed.path, '', {'X-Auth-Token': token})
       
   652 +            return check_response(conn)
       
   653 +        resp = retry(head)
       
   654 +        self.existing_metadata = set([
       
   655 +            k for k, v in resp.getheaders() if
       
   656 +            k.lower().startswith('x-account-meta')])
       
   657 +
       
   658 +    def tearDown(self):
       
   659 +        def head(url, token, parsed, conn):
       
   660 +            conn.request('HEAD', parsed.path, '', {'X-Auth-Token': token})
       
   661 +            return check_response(conn)
       
   662 +        resp = retry(head)
       
   663 +        resp.read()
       
   664 +        new_metadata = set(
       
   665 +            [k for k, v in resp.getheaders() if
       
   666 +             k.lower().startswith('x-account-meta')])
       
   667 +
       
   668 +        def clear_meta(url, token, parsed, conn, remove_metadata_keys):
       
   669 +            headers = {'X-Auth-Token': token}
       
   670 +            headers.update((k, '') for k in remove_metadata_keys)
       
   671 +            conn.request('POST', parsed.path, '', headers)
       
   672 +            return check_response(conn)
       
   673 +        extra_metadata = list(self.existing_metadata ^ new_metadata)
       
   674 +        for i in range(0, len(extra_metadata), 90):
       
   675 +            batch = extra_metadata[i:i + 90]
       
   676 +            resp = retry(clear_meta, batch)
       
   677 +            resp.read()
       
   678 +            self.assertEqual(resp.status // 100, 2)
       
   679 +
       
   680 +    def test_maxcount_metadata(self):
       
   681 +        if skip:
       
   682 +            raise SkipTest
       
   683 +
       
   684 +        def post(url, token, parsed, conn, extra_headers):
       
   685 +            headers = {'X-Auth-Token': token}
       
   686 +            headers.update(extra_headers)
       
   687 +            conn.request('POST', parsed.path, '', headers)
       
   688 +            return check_response(conn)
       
   689 +
       
   690 +        # TODO: Find the test that adds these and remove them.
       
   691 +        headers = {'x-remove-account-meta-temp-url-key': 'remove',
       
   692 +                   'x-remove-account-meta-temp-url-key-2': 'remove'}
       
   693 +        resp = retry(post, headers)
       
   694 +
       
   695 +        headers = {}
       
   696 +        for x in xrange(MAX_META_COUNT):
       
   697 +            headers['X-Account-Meta-%d' % x] = 'v'
       
   698 +        resp = retry(post, headers)
       
   699 +        resp.read()
       
   700 +        self.assertEqual(resp.status, 204)
       
   701 +
       
   702 +    def test_maxcount_metadata_over_one(self):
       
   703 +        if skip:
       
   704 +            raise SkipTest
       
   705 +
       
   706 +        def post(url, token, parsed, conn, extra_headers):
       
   707 +            headers = {'X-Auth-Token': token}
       
   708 +            headers.update(extra_headers)
       
   709 +            conn.request('POST', parsed.path, '', headers)
       
   710 +            return check_response(conn)
       
   711 +
       
   712 +        # TODO: Find the test that adds these and remove them.
       
   713 +        headers = {'x-remove-account-meta-temp-url-key': 'remove',
       
   714 +                   'x-remove-account-meta-temp-url-key-2': 'remove'}
       
   715 +        resp = retry(post, headers)
       
   716 +
       
   717 +        headers = {}
       
   718 +        for x in xrange(MAX_META_COUNT + 1):
       
   719 +            headers['X-Account-Meta-%d' % x] = 'v'
       
   720 +        resp = retry(post, headers)
       
   721 +        resp.read()
       
   722 +        self.assertEqual(resp.status, 400)
       
   723 +
       
   724 +    def test_maxcount_metadata_over_two(self):
       
   725 +        if skip:
       
   726 +            raise SkipTest
       
   727 +
       
   728 +        def post(url, token, parsed, conn, extra_headers):
       
   729 +            headers = {'X-Auth-Token': token}
       
   730 +            headers.update(extra_headers)
       
   731 +            conn.request('POST', parsed.path, '', headers)
       
   732 +            return check_response(conn)
       
   733 +
       
   734 +        # TODO: Find the test that adds these and remove them.
       
   735 +        headers = {'x-remove-account-meta-temp-url-key': 'remove',
       
   736 +                   'x-remove-account-meta-temp-url-key-2': 'remove'}
       
   737 +        resp = retry(post, headers)
       
   738 +
       
   739 +        headers = {}
       
   740 +        for x in xrange(MAX_META_COUNT - 1):
       
   741 +            headers['X-Account-Meta-%d' % x] = 'v'
       
   742 +        resp = retry(post, headers)
       
   743 +        resp.read()
       
   744 +        self.assertEqual(resp.status, 204)
       
   745 +
       
   746 +        headers = {}
       
   747 +        for x in xrange(MAX_META_COUNT - 1, MAX_META_COUNT + 1):
       
   748 +            headers['X-Account-Meta-%d' % x] = 'v'
       
   749 +        resp = retry(post, headers)
       
   750 +        resp.read()
       
   751 +        self.assertEqual(resp.status, 400)
       
   752 +
       
   753 +    def test_maxsize_metadata(self):
       
   754 +        if skip:
       
   755 +            raise SkipTest
       
   756 +
       
   757 +        def post(url, token, parsed, conn, extra_headers):
       
   758 +            headers = {'X-Auth-Token': token}
       
   759 +            headers.update(extra_headers)
       
   760 +            conn.request('POST', parsed.path, '', headers)
       
   761 +            return check_response(conn)
       
   762 +
       
   763 +        # TODO: Find the test that adds these and remove them.
       
   764 +        headers = {'x-remove-account-meta-temp-url-key': 'remove',
       
   765 +                   'x-remove-account-meta-temp-url-key-2': 'remove'}
       
   766 +        resp = retry(post, headers)
       
   767 +
       
   768 +        headers = {}
       
   769 +        header_value = 'k' * MAX_META_VALUE_LENGTH
       
   770 +        size = 0
       
   771 +        x = 0
       
   772 +        while size < MAX_META_OVERALL_SIZE - 4 - MAX_META_VALUE_LENGTH:
       
   773 +            size += 4 + MAX_META_VALUE_LENGTH
       
   774 +            headers['X-Account-Meta-%04d' % x] = header_value
       
   775 +            x += 1
       
   776 +        if MAX_META_OVERALL_SIZE - size > 1:
       
   777 +            headers['X-Account-Meta-k'] = \
       
   778 +                'v' * (MAX_META_OVERALL_SIZE - size - 1)
       
   779 +        resp = retry(post, headers)
       
   780 +        resp.read()
       
   781 +        self.assertEqual(resp.status, 204)
       
   782 +        headers['X-Account-Meta-k'] = \
       
   783 +            'v' * (MAX_META_OVERALL_SIZE - size)
       
   784 +        resp = retry(post, headers)
       
   785 +        resp.read()
       
   786 +        self.assertEqual(resp.status, 400)
       
   787 +
       
   788 +    def test_maxsize_metadata_over_one(self):
       
   789 +        if skip:
       
   790 +            raise SkipTest
       
   791 +
       
   792 +        def post(url, token, parsed, conn, extra_headers):
       
   793 +            headers = {'X-Auth-Token': token}
       
   794 +            headers.update(extra_headers)
       
   795 +            conn.request('POST', parsed.path, '', headers)
       
   796 +            return check_response(conn)
       
   797 +
       
   798 +        # TODO: Find the test that adds these and remove them.
       
   799 +        headers = {'x-remove-account-meta-temp-url-key': 'remove',
       
   800 +                   'x-remove-account-meta-temp-url-key-2': 'remove'}
       
   801 +        resp = retry(post, headers)
       
   802 +
       
   803 +        headers = {}
       
   804 +        header_value = 'k' * MAX_META_VALUE_LENGTH
       
   805 +        size = 0
       
   806 +        x = 0
       
   807 +        while size < MAX_META_OVERALL_SIZE - 4 - MAX_META_VALUE_LENGTH:
       
   808 +            size += 4 + MAX_META_VALUE_LENGTH
       
   809 +            headers['X-Account-Meta-%04d' % x] = header_value
       
   810 +            x += 1
       
   811 +        if MAX_META_OVERALL_SIZE >= size:
       
   812 +            headers['X-Account-Meta-k'] = \
       
   813 +                'v' * (MAX_META_OVERALL_SIZE - size)
       
   814 +        resp = retry(post, headers)
       
   815 +        resp.read()
       
   816 +        self.assertEqual(resp.status, 400)
       
   817 +
       
   818 +    def test_maxsize_metadata_over_two(self):
       
   819 +        if skip:
       
   820 +            raise SkipTest
       
   821 +
       
   822 +        def post(url, token, parsed, conn, extra_headers):
       
   823 +            headers = {'X-Auth-Token': token}
       
   824 +            headers.update(extra_headers)
       
   825 +            conn.request('POST', parsed.path, '', headers)
       
   826 +            return check_response(conn)
       
   827 +
       
   828 +        # TODO: Find the test that adds these and remove them.
       
   829 +        headers = {'x-remove-account-meta-temp-url-key': 'remove',
       
   830 +                   'x-remove-account-meta-temp-url-key-2': 'remove'}
       
   831 +        resp = retry(post, headers)
       
   832 +
       
   833 +        headers = {}
       
   834 +        header_value = 'k' * MAX_META_VALUE_LENGTH
       
   835 +        size = 0
       
   836 +        x = 0
       
   837 +        while size < MAX_META_OVERALL_SIZE - 4 - MAX_META_VALUE_LENGTH:
       
   838 +            size += 4 + MAX_META_VALUE_LENGTH
       
   839 +            headers['X-Account-Meta-%04d' % x] = header_value
       
   840 +            x += 1
       
   841 +        if MAX_META_OVERALL_SIZE - size > 1:
       
   842 +            headers['X-Account-Meta-k'] = \
       
   843 +                'v' * (MAX_META_OVERALL_SIZE - size - 1)
       
   844 +        resp = retry(post, headers)
       
   845 +        resp.read()
       
   846 +        self.assertEqual(resp.status, 204)
       
   847 +        headers = {}
       
   848 +        headers['X-Account-Meta-k2'] = 'v'
       
   849 +        resp = retry(post, headers)
       
   850 +        resp.read()
       
   851 +        self.assertEqual(resp.status, 400)
       
   852 +
       
   853  if __name__ == '__main__':
       
   854      unittest.main()
       
   855 --- a/test/unit/common/test_db.py
       
   856 +++ b/test/unit/common/test_db.py
       
   857 @@ -26,10 +26,13 @@ import sqlite3
       
   858  from mock import patch
       
   859  
       
   860  import swift.common.db
       
   861 +from swift.common.constraints import \
       
   862 +    MAX_META_VALUE_LENGTH, MAX_META_COUNT, MAX_META_OVERALL_SIZE
       
   863  from swift.common.db import chexor, dict_factory, get_db_connection, \
       
   864      DatabaseBroker, DatabaseConnectionError, DatabaseAlreadyExists
       
   865  from swift.common.utils import normalize_timestamp
       
   866  from swift.common.exceptions import LockTimeout
       
   867 +from swift.common.swob import HTTPException
       
   868  
       
   869  
       
   870  class TestDatabaseConnectionError(unittest.TestCase):
       
   871 @@ -181,7 +184,7 @@ class TestDatabaseBroker(unittest.TestCa
       
   872              conn.execute('CREATE TABLE test (one TEXT)')
       
   873              conn.execute('CREATE TABLE test_stat (id TEXT)')
       
   874              conn.execute('INSERT INTO test_stat (id) VALUES (?)',
       
   875 -                        (str(uuid4),))
       
   876 +                         (str(uuid4),))
       
   877              conn.execute('INSERT INTO test (one) VALUES ("1")')
       
   878              conn.commit()
       
   879          stub_called = [False]
       
   880 @@ -632,6 +635,91 @@ class TestDatabaseBroker(unittest.TestCa
       
   881                            [first_value, first_timestamp])
       
   882          self.assert_('Second' not in broker.metadata)
       
   883  
       
   884 +    @patch.object(DatabaseBroker, 'validate_metadata')
       
   885 +    def test_validate_metadata_is_called_from_update_metadata(self, mock):
       
   886 +        broker = self.get_replication_info_tester(metadata=True)
       
   887 +        first_timestamp = normalize_timestamp(1)
       
   888 +        first_value = '1'
       
   889 +        metadata = {'First': [first_value, first_timestamp]}
       
   890 +        broker.update_metadata(metadata, validate_metadata=True)
       
   891 +        self.assertTrue(mock.called)
       
   892 +
       
   893 +    @patch.object(DatabaseBroker, 'validate_metadata')
       
   894 +    def test_validate_metadata_is_not_called_from_update_metadata(self, mock):
       
   895 +        broker = self.get_replication_info_tester(metadata=True)
       
   896 +        first_timestamp = normalize_timestamp(1)
       
   897 +        first_value = '1'
       
   898 +        metadata = {'First': [first_value, first_timestamp]}
       
   899 +        broker.update_metadata(metadata)
       
   900 +        self.assertFalse(mock.called)
       
   901 +
       
   902 +    def test_metadata_with_max_count(self):
       
   903 +        metadata = {}
       
   904 +        for c in xrange(MAX_META_COUNT):
       
   905 +            key = 'X-Account-Meta-F{0}'.format(c)
       
   906 +            metadata[key] = ('B', normalize_timestamp(1))
       
   907 +        key = 'X-Account-Meta-Foo'.format(c)
       
   908 +        metadata[key] = ('', normalize_timestamp(1))
       
   909 +        try:
       
   910 +            DatabaseBroker.validate_metadata(metadata)
       
   911 +        except HTTPException:
       
   912 +            self.fail('Unexpected HTTPException')
       
   913 +
       
   914 +    def test_metadata_raises_exception_over_max_count(self):
       
   915 +        metadata = {}
       
   916 +        for c in xrange(MAX_META_COUNT + 1):
       
   917 +            key = 'X-Account-Meta-F{0}'.format(c)
       
   918 +            metadata[key] = ('B', normalize_timestamp(1))
       
   919 +        message = ''
       
   920 +        try:
       
   921 +            DatabaseBroker.validate_metadata(metadata)
       
   922 +        except HTTPException as e:
       
   923 +            message = str(e)
       
   924 +        self.assertEqual(message, '400 Bad Request')
       
   925 +
       
   926 +    def test_metadata_with_max_overall_size(self):
       
   927 +        metadata = {}
       
   928 +        metadata_value = 'v' * MAX_META_VALUE_LENGTH
       
   929 +        size = 0
       
   930 +        x = 0
       
   931 +        while size < (MAX_META_OVERALL_SIZE - 4
       
   932 +                      - MAX_META_VALUE_LENGTH):
       
   933 +            size += 4 + MAX_META_VALUE_LENGTH
       
   934 +            metadata['X-Account-Meta-%04d' % x] = (metadata_value,
       
   935 +                                                   normalize_timestamp(1))
       
   936 +            x += 1
       
   937 +        if MAX_META_OVERALL_SIZE - size > 1:
       
   938 +            metadata['X-Account-Meta-k'] = (
       
   939 +                'v' * (MAX_META_OVERALL_SIZE - size - 1),
       
   940 +                normalize_timestamp(1))
       
   941 +        try:
       
   942 +            DatabaseBroker.validate_metadata(metadata)
       
   943 +        except HTTPException:
       
   944 +            self.fail('Unexpected HTTPException')
       
   945 +
       
   946 +    def test_metadata_raises_exception_over_max_overall_size(self):
       
   947 +        metadata = {}
       
   948 +        metadata_value = 'k' * MAX_META_VALUE_LENGTH
       
   949 +        size = 0
       
   950 +        x = 0
       
   951 +        while size < (MAX_META_OVERALL_SIZE - 4
       
   952 +                      - MAX_META_VALUE_LENGTH):
       
   953 +            size += 4 + MAX_META_VALUE_LENGTH
       
   954 +            metadata['X-Account-Meta-%04d' % x] = (metadata_value,
       
   955 +                                                   normalize_timestamp(1))
       
   956 +            x += 1
       
   957 +        if MAX_META_OVERALL_SIZE - size > 1:
       
   958 +            metadata['X-Account-Meta-k'] = (
       
   959 +                'v' * (MAX_META_OVERALL_SIZE - size - 1),
       
   960 +                normalize_timestamp(1))
       
   961 +        metadata['X-Account-Meta-k2'] = ('v', normalize_timestamp(1))
       
   962 +        message = ''
       
   963 +        try:
       
   964 +            DatabaseBroker.validate_metadata(metadata)
       
   965 +        except HTTPException as e:
       
   966 +            message = str(e)
       
   967 +        self.assertEqual(message, '400 Bad Request')
       
   968 +
       
   969  
       
   970  if __name__ == '__main__':
       
   971      unittest.main()