components/openstack/swift/patches/01-CVE-2013-4155.patch
changeset 1896 f83e6dde6c3b
equal deleted inserted replaced
1895:1f133713df64 1896:f83e6dde6c3b
       
     1 commit 1f4ec235cdfd8c868f2d6458532f9dc32c00b8ca
       
     2 Author: Peter Portante <[email protected]>
       
     3 Date:   Fri Jul 26 15:03:34 2013 -0400
       
     4 
       
     5     Fix handling of DELETE obj reqs with old timestamp
       
     6     
       
     7     The DELETE object REST API was creating tombstone files with old
       
     8     timestamps, potentially filling up the disk, as well as sending
       
     9     container updates.
       
    10     
       
    11     Here we now make DELETEs with a request timestamp return a 409 (HTTP
       
    12     Conflict) if a data file exists with a newer timestamp, only creating
       
    13     tombstones if they have a newer timestamp.
       
    14     
       
    15     The key fix is to actually read the timestamp metadata from an
       
    16     existing tombstone file (thanks to Pete Zaitcev for catching this),
       
    17     and then only create tombstone files with newer timestamps.
       
    18     
       
    19     We also prevent PUT and POST operations using old timestamps as well.
       
    20     
       
    21     Change-Id: I631957029d17c6578bca5779367df5144ba01fc9
       
    22     Signed-off-by: Peter Portante <[email protected]>
       
    23 
       
    24 --- a/swift/obj/server.py
       
    25 +++ b/swift/obj/server.py
       
    26 @@ -46,7 +46,7 @@ from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPCreated, \
       
    27      HTTPInternalServerError, HTTPNoContent, HTTPNotFound, HTTPNotModified, \
       
    28      HTTPPreconditionFailed, HTTPRequestTimeout, HTTPUnprocessableEntity, \
       
    29      HTTPClientDisconnect, HTTPMethodNotAllowed, Request, Response, UTC, \
       
    30 -    HTTPInsufficientStorage, multi_range_iterator
       
    31 +    HTTPInsufficientStorage, multi_range_iterator, HTTPConflict
       
    32  
       
    33  
       
    34  DATADIR = 'objects'
       
    35 @@ -121,7 +121,6 @@ class DiskFile(object):
       
    36          self.tmppath = None
       
    37          self.logger = logger
       
    38          self.metadata = {}
       
    39 -        self.meta_file = None
       
    40          self.data_file = None
       
    41          self.fp = None
       
    42          self.iter_etag = None
       
    43 @@ -133,15 +132,18 @@ class DiskFile(object):
       
    44          if not os.path.exists(self.datadir):
       
    45              return
       
    46          files = sorted(os.listdir(self.datadir), reverse=True)
       
    47 -        for file in files:
       
    48 -            if file.endswith('.ts'):
       
    49 -                self.data_file = self.meta_file = None
       
    50 -                self.metadata = {'deleted': True}
       
    51 -                return
       
    52 -            if file.endswith('.meta') and not self.meta_file:
       
    53 -                self.meta_file = os.path.join(self.datadir, file)
       
    54 -            if file.endswith('.data') and not self.data_file:
       
    55 -                self.data_file = os.path.join(self.datadir, file)
       
    56 +        meta_file = None
       
    57 +        for afile in files:
       
    58 +            if afile.endswith('.ts'):
       
    59 +                self.data_file = None
       
    60 +                with open(os.path.join(self.datadir, afile)) as mfp:
       
    61 +                    self.metadata = read_metadata(mfp)
       
    62 +                self.metadata['deleted'] = True
       
    63 +                break
       
    64 +            if afile.endswith('.meta') and not meta_file:
       
    65 +                meta_file = os.path.join(self.datadir, afile)
       
    66 +            if afile.endswith('.data') and not self.data_file:
       
    67 +                self.data_file = os.path.join(self.datadir, afile)
       
    68                  break
       
    69          if not self.data_file:
       
    70              return
       
    71 @@ -149,8 +151,8 @@ class DiskFile(object):
       
    72          self.metadata = read_metadata(self.fp)
       
    73          if not keep_data_fp:
       
    74              self.close(verify_file=False)
       
    75 -        if self.meta_file:
       
    76 -            with open(self.meta_file) as mfp:
       
    77 +        if meta_file:
       
    78 +            with open(meta_file) as mfp:
       
    79                  for key in self.metadata.keys():
       
    80                      if key.lower() not in DISALLOWED_HEADERS:
       
    81                          del self.metadata[key]
       
    82 @@ -594,6 +596,9 @@ class ObjectController(object):
       
    83          except (DiskFileError, DiskFileNotExist):
       
    84              file.quarantine()
       
    85              return HTTPNotFound(request=request)
       
    86 +        orig_timestamp = file.metadata.get('X-Timestamp', '0')
       
    87 +        if orig_timestamp >= request.headers['x-timestamp']:
       
    88 +            return HTTPConflict(request=request)
       
    89          metadata = {'X-Timestamp': request.headers['x-timestamp']}
       
    90          metadata.update(val for val in request.headers.iteritems()
       
    91                          if val[0].lower().startswith('x-object-meta-'))
       
    92 @@ -639,6 +644,8 @@ class ObjectController(object):
       
    93          file = DiskFile(self.devices, device, partition, account, container,
       
    94                          obj, self.logger, disk_chunk_size=self.disk_chunk_size)
       
    95          orig_timestamp = file.metadata.get('X-Timestamp')
       
    96 +        if orig_timestamp and orig_timestamp >= request.headers['x-timestamp']:
       
    97 +            return HTTPConflict(request=request)
       
    98          upload_expiration = time.time() + self.max_upload_time
       
    99          etag = md5()
       
   100          upload_size = 0
       
   101 @@ -863,23 +870,26 @@ class ObjectController(object):
       
   102              return HTTPPreconditionFailed(
       
   103                  request=request,
       
   104                  body='X-If-Delete-At and X-Delete-At do not match')
       
   105 -        orig_timestamp = file.metadata.get('X-Timestamp')
       
   106 -        if file.is_deleted() or file.is_expired():
       
   107 -            response_class = HTTPNotFound
       
   108 -        metadata = {
       
   109 -            'X-Timestamp': request.headers['X-Timestamp'], 'deleted': True,
       
   110 -        }
       
   111          old_delete_at = int(file.metadata.get('X-Delete-At') or 0)
       
   112          if old_delete_at:
       
   113              self.delete_at_update('DELETE', old_delete_at, account,
       
   114                                    container, obj, request.headers, device)
       
   115 -        file.put_metadata(metadata, tombstone=True)
       
   116 -        file.unlinkold(metadata['X-Timestamp'])
       
   117 -        if not orig_timestamp or \
       
   118 -                orig_timestamp < request.headers['x-timestamp']:
       
   119 +        orig_timestamp = file.metadata.get('X-Timestamp', 0)
       
   120 +        req_timestamp = request.headers['X-Timestamp']
       
   121 +        if file.is_deleted() or file.is_expired():
       
   122 +            response_class = HTTPNotFound
       
   123 +        else:
       
   124 +            if orig_timestamp < req_timestamp:
       
   125 +                response_class = HTTPNoContent
       
   126 +            else:
       
   127 +                response_class = HTTPConflict
       
   128 +        if orig_timestamp < req_timestamp:
       
   129 +            file.put_metadata({'X-Timestamp': req_timestamp},
       
   130 +                              tombstone=True)
       
   131 +            file.unlinkold(req_timestamp)
       
   132              self.container_update(
       
   133                  'DELETE', account, container, obj, request.headers,
       
   134 -                {'x-timestamp': metadata['X-Timestamp'],
       
   135 +                {'x-timestamp': req_timestamp,
       
   136                   'x-trans-id': request.headers.get('x-trans-id', '-')},
       
   137                  device)
       
   138          resp = response_class(request=request)
       
   139 
       
   140 
       
   141 diff --git a/test/unit/obj/test_server.py b/test/unit/obj/test_server.py
       
   142 
       
   143 index 8ee266b..b354b97 100755 (executable)
       
   144 
       
   145 
       
   146 --- a/test/unit/obj/test_server.py
       
   147 +++ b/test/unit/obj/test_server.py
       
   148 @@ -509,6 +509,41 @@ class TestObjectController(unittest.TestCase):
       
   149                       "X-Object-Meta-3" in resp.headers)
       
   150          self.assertEquals(resp.headers['Content-Type'], 'application/x-test')
       
   151  
       
   152 +    def test_POST_old_timestamp(self):
       
   153 +        ts = time()
       
   154 +        timestamp = normalize_timestamp(ts)
       
   155 +        req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
       
   156 +                            headers={'X-Timestamp': timestamp,
       
   157 +                                     'Content-Type': 'application/x-test',
       
   158 +                                     'X-Object-Meta-1': 'One',
       
   159 +                                     'X-Object-Meta-Two': 'Two'})
       
   160 +        req.body = 'VERIFY'
       
   161 +        resp = self.object_controller.PUT(req)
       
   162 +        self.assertEquals(resp.status_int, 201)
       
   163 +
       
   164 +        # Same timestamp should result in 409
       
   165 +        req = Request.blank('/sda1/p/a/c/o',
       
   166 +                            environ={'REQUEST_METHOD': 'POST'},
       
   167 +                            headers={'X-Timestamp': timestamp,
       
   168 +                                     'X-Object-Meta-3': 'Three',
       
   169 +                                     'X-Object-Meta-4': 'Four',
       
   170 +                                     'Content-Encoding': 'gzip',
       
   171 +                                     'Content-Type': 'application/x-test'})
       
   172 +        resp = self.object_controller.POST(req)
       
   173 +        self.assertEquals(resp.status_int, 409)
       
   174 +
       
   175 +        # Earlier timestamp should result in 409
       
   176 +        timestamp = normalize_timestamp(ts - 1)
       
   177 +        req = Request.blank('/sda1/p/a/c/o',
       
   178 +                            environ={'REQUEST_METHOD': 'POST'},
       
   179 +                            headers={'X-Timestamp': timestamp,
       
   180 +                                     'X-Object-Meta-5': 'Five',
       
   181 +                                     'X-Object-Meta-6': 'Six',
       
   182 +                                     'Content-Encoding': 'gzip',
       
   183 +                                     'Content-Type': 'application/x-test'})
       
   184 +        resp = self.object_controller.POST(req)
       
   185 +        self.assertEquals(resp.status_int, 409)
       
   186 +
       
   187      def test_POST_not_exist(self):
       
   188          timestamp = normalize_timestamp(time())
       
   189          req = Request.blank('/sda1/p/a/c/fail',
       
   190 @@ -555,11 +590,15 @@ class TestObjectController(unittest.TestCase):
       
   191  
       
   192          old_http_connect = object_server.http_connect
       
   193          try:
       
   194 -            timestamp = normalize_timestamp(time())
       
   195 +            ts = time()
       
   196 +            timestamp = normalize_timestamp(ts)
       
   197              req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD':
       
   198                  'POST'}, headers={'X-Timestamp': timestamp, 'Content-Type':
       
   199                  'text/plain', 'Content-Length': '0'})
       
   200              resp = self.object_controller.PUT(req)
       
   201 +            self.assertEquals(resp.status_int, 201)
       
   202 +
       
   203 +            timestamp = normalize_timestamp(ts + 1)
       
   204              req = Request.blank('/sda1/p/a/c/o',
       
   205                      environ={'REQUEST_METHOD': 'POST'},
       
   206                      headers={'X-Timestamp': timestamp,
       
   207 @@ -571,6 +610,8 @@ class TestObjectController(unittest.TestCase):
       
   208              object_server.http_connect = mock_http_connect(202)
       
   209              resp = self.object_controller.POST(req)
       
   210              self.assertEquals(resp.status_int, 202)
       
   211 +
       
   212 +            timestamp = normalize_timestamp(ts + 2)
       
   213              req = Request.blank('/sda1/p/a/c/o',
       
   214                      environ={'REQUEST_METHOD': 'POST'},
       
   215                      headers={'X-Timestamp': timestamp,
       
   216 @@ -582,6 +623,8 @@ class TestObjectController(unittest.TestCase):
       
   217              object_server.http_connect = mock_http_connect(202, with_exc=True)
       
   218              resp = self.object_controller.POST(req)
       
   219              self.assertEquals(resp.status_int, 202)
       
   220 +
       
   221 +            timestamp = normalize_timestamp(ts + 3)
       
   222              req = Request.blank('/sda1/p/a/c/o',
       
   223                      environ={'REQUEST_METHOD': 'POST'},
       
   224                      headers={'X-Timestamp': timestamp,
       
   225 @@ -718,6 +761,32 @@ class TestObjectController(unittest.TestCase):
       
   226                             'name': '/a/c/o',
       
   227                             'Content-Encoding': 'gzip'})
       
   228  
       
   229 +    def test_PUT_old_timestamp(self):
       
   230 +        ts = time()
       
   231 +        req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
       
   232 +                headers={'X-Timestamp': normalize_timestamp(ts),
       
   233 +                         'Content-Length': '6',
       
   234 +                         'Content-Type': 'application/octet-stream'})
       
   235 +        req.body = 'VERIFY'
       
   236 +        resp = self.object_controller.PUT(req)
       
   237 +        self.assertEquals(resp.status_int, 201)
       
   238 +
       
   239 +        req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
       
   240 +                            headers={'X-Timestamp': normalize_timestamp(ts),
       
   241 +                                     'Content-Type': 'text/plain',
       
   242 +                                     'Content-Encoding': 'gzip'})
       
   243 +        req.body = 'VERIFY TWO'
       
   244 +        resp = self.object_controller.PUT(req)
       
   245 +        self.assertEquals(resp.status_int, 409)
       
   246 +
       
   247 +        req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
       
   248 +                            headers={'X-Timestamp': normalize_timestamp(ts - 1),
       
   249 +                                     'Content-Type': 'text/plain',
       
   250 +                                     'Content-Encoding': 'gzip'})
       
   251 +        req.body = 'VERIFY THREE'
       
   252 +        resp = self.object_controller.PUT(req)
       
   253 +        self.assertEquals(resp.status_int, 409)
       
   254 +
       
   255      def test_PUT_no_etag(self):
       
   256          req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
       
   257                             headers={'X-Timestamp': normalize_timestamp(time()),
       
   258 @@ -1306,12 +1375,32 @@ class TestObjectController(unittest.TestCase):
       
   259          self.assertEquals(resp.status_int, 400)
       
   260          # self.assertRaises(KeyError, self.object_controller.DELETE, req)
       
   261  
       
   262 +        # The following should have created a tombstone file
       
   263          timestamp = normalize_timestamp(time())
       
   264          req = Request.blank('/sda1/p/a/c/o',
       
   265                              environ={'REQUEST_METHOD': 'DELETE'},
       
   266                              headers={'X-Timestamp': timestamp})
       
   267          resp = self.object_controller.DELETE(req)
       
   268          self.assertEquals(resp.status_int, 404)
       
   269 +        objfile = os.path.join(self.testdir, 'sda1',
       
   270 +            storage_directory(object_server.DATADIR, 'p',
       
   271 +                              hash_path('a', 'c', 'o')),
       
   272 +            timestamp + '.ts')
       
   273 +        self.assert_(os.path.isfile(objfile))
       
   274 +
       
   275 +        # The following should *not* have created a tombstone file.
       
   276 +        timestamp = normalize_timestamp(float(timestamp) - 1)
       
   277 +        req = Request.blank('/sda1/p/a/c/o',
       
   278 +                            environ={'REQUEST_METHOD': 'DELETE'},
       
   279 +                            headers={'X-Timestamp': timestamp})
       
   280 +        resp = self.object_controller.DELETE(req)
       
   281 +        self.assertEquals(resp.status_int, 404)
       
   282 +        objfile = os.path.join(self.testdir, 'sda1',
       
   283 +            storage_directory(object_server.DATADIR, 'p',
       
   284 +                              hash_path('a', 'c', 'o')),
       
   285 +            timestamp + '.ts')
       
   286 +        self.assertFalse(os.path.exists(objfile))
       
   287 +        self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
       
   288  
       
   289          sleep(.00001)
       
   290          timestamp = normalize_timestamp(time())
       
   291 @@ -1325,17 +1414,19 @@ class TestObjectController(unittest.TestCase):
       
   292          resp = self.object_controller.PUT(req)
       
   293          self.assertEquals(resp.status_int, 201)
       
   294  
       
   295 +        # The following should *not* have created a tombstone file.
       
   296          timestamp = normalize_timestamp(float(timestamp) - 1)
       
   297          req = Request.blank('/sda1/p/a/c/o',
       
   298                              environ={'REQUEST_METHOD': 'DELETE'},
       
   299                              headers={'X-Timestamp': timestamp})
       
   300          resp = self.object_controller.DELETE(req)
       
   301 -        self.assertEquals(resp.status_int, 204)
       
   302 +        self.assertEquals(resp.status_int, 409)
       
   303          objfile = os.path.join(self.testdir, 'sda1',
       
   304              storage_directory(object_server.DATADIR, 'p',
       
   305                                hash_path('a', 'c', 'o')),
       
   306              timestamp + '.ts')
       
   307 -        self.assert_(os.path.isfile(objfile))
       
   308 +        self.assertFalse(os.path.exists(objfile))
       
   309 +        self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
       
   310  
       
   311          sleep(.00001)
       
   312          timestamp = normalize_timestamp(time())
       
   313 @@ -1350,6 +1441,103 @@ class TestObjectController(unittest.TestCase):
       
   314              timestamp + '.ts')
       
   315          self.assert_(os.path.isfile(objfile))
       
   316  
       
   317 +    def test_DELETE_container_updates(self):
       
   318 +        # Test swift.object_server.ObjectController.DELETE and container
       
   319 +        # updates, making sure container update is called in the correct
       
   320 +        # state.
       
   321 +        timestamp = normalize_timestamp(time())
       
   322 +        req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
       
   323 +                            headers={
       
   324 +                                'X-Timestamp': timestamp,
       
   325 +                                'Content-Type': 'application/octet-stream',
       
   326 +                                'Content-Length': '4',
       
   327 +                                })
       
   328 +        req.body = 'test'
       
   329 +        resp = self.object_controller.PUT(req)
       
   330 +        self.assertEquals(resp.status_int, 201)
       
   331 +
       
   332 +        calls_made = [0]
       
   333 +
       
   334 +        def our_container_update(*args, **kwargs):
       
   335 +            calls_made[0] += 1
       
   336 +
       
   337 +        orig_cu = self.object_controller.container_update
       
   338 +        self.object_controller.container_update = our_container_update
       
   339 +        try:
       
   340 +            # The following request should return 409 (HTTP Conflict). A
       
   341 +            # tombstone file should not have been created with this timestamp.
       
   342 +            timestamp = normalize_timestamp(float(timestamp) - 1)
       
   343 +            req = Request.blank('/sda1/p/a/c/o',
       
   344 +                                environ={'REQUEST_METHOD': 'DELETE'},
       
   345 +                                headers={'X-Timestamp': timestamp})
       
   346 +            resp = self.object_controller.DELETE(req)
       
   347 +            self.assertEquals(resp.status_int, 409)
       
   348 +            objfile = os.path.join(self.testdir, 'sda1',
       
   349 +                storage_directory(object_server.DATADIR, 'p',
       
   350 +                                  hash_path('a', 'c', 'o')),
       
   351 +                timestamp + '.ts')
       
   352 +            self.assertFalse(os.path.isfile(objfile))
       
   353 +            self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
       
   354 +            self.assertEquals(0, calls_made[0])
       
   355 +
       
   356 +            # The following request should return 204, and the object should
       
   357 +            # be truly deleted (container update is performed) because this
       
   358 +            # timestamp is newer. A tombstone file should have been created
       
   359 +            # with this timestamp.
       
   360 +            sleep(.00001)
       
   361 +            timestamp = normalize_timestamp(time())
       
   362 +            req = Request.blank('/sda1/p/a/c/o',
       
   363 +                                environ={'REQUEST_METHOD': 'DELETE'},
       
   364 +                                headers={'X-Timestamp': timestamp})
       
   365 +            resp = self.object_controller.DELETE(req)
       
   366 +            self.assertEquals(resp.status_int, 204)
       
   367 +            objfile = os.path.join(self.testdir, 'sda1',
       
   368 +                storage_directory(object_server.DATADIR, 'p',
       
   369 +                                  hash_path('a', 'c', 'o')),
       
   370 +                timestamp + '.ts')
       
   371 +            self.assert_(os.path.isfile(objfile))
       
   372 +            self.assertEquals(1, calls_made[0])
       
   373 +            self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
       
   374 +
       
   375 +            # The following request should return a 404, as the object should
       
   376 +            # already have been deleted, but it should have also performed a
       
   377 +            # container update because the timestamp is newer, and a tombstone
       
   378 +            # file should also exist with this timestamp.
       
   379 +            sleep(.00001)
       
   380 +            timestamp = normalize_timestamp(time())
       
   381 +            req = Request.blank('/sda1/p/a/c/o',
       
   382 +                                environ={'REQUEST_METHOD': 'DELETE'},
       
   383 +                                headers={'X-Timestamp': timestamp})
       
   384 +            resp = self.object_controller.DELETE(req)
       
   385 +            self.assertEquals(resp.status_int, 404)
       
   386 +            objfile = os.path.join(self.testdir, 'sda1',
       
   387 +                storage_directory(object_server.DATADIR, 'p',
       
   388 +                                  hash_path('a', 'c', 'o')),
       
   389 +                timestamp + '.ts')
       
   390 +            self.assert_(os.path.isfile(objfile))
       
   391 +            self.assertEquals(2, calls_made[0])
       
   392 +            self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
       
   393 +
       
   394 +            # The following request should return a 404, as the object should
       
   395 +            # already have been deleted, and it should not have performed a
       
   396 +            # container update because the timestamp is older, or created a
       
   397 +            # tombstone file with this timestamp.
       
   398 +            timestamp = normalize_timestamp(float(timestamp) - 1)
       
   399 +            req = Request.blank('/sda1/p/a/c/o',
       
   400 +                                environ={'REQUEST_METHOD': 'DELETE'},
       
   401 +                                headers={'X-Timestamp': timestamp})
       
   402 +            resp = self.object_controller.DELETE(req)
       
   403 +            self.assertEquals(resp.status_int, 404)
       
   404 +            objfile = os.path.join(self.testdir, 'sda1',
       
   405 +                storage_directory(object_server.DATADIR, 'p',
       
   406 +                                  hash_path('a', 'c', 'o')),
       
   407 +                timestamp + '.ts')
       
   408 +            self.assertFalse(os.path.isfile(objfile))
       
   409 +            self.assertEquals(2, calls_made[0])
       
   410 +            self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
       
   411 +        finally:
       
   412 +            self.object_controller.container_update = orig_cu
       
   413 +
       
   414      def test_call(self):
       
   415          """ Test swift.object_server.ObjectController.__call__ """
       
   416          inbuf = StringIO()