Moderate severityNVD Advisory· Published Aug 20, 2013· Updated Apr 29, 2026
CVE-2013-4155
CVE-2013-4155
Description
OpenStack Swift before 1.9.1 in Folsom, Grizzly, and Havana allows authenticated users to cause a denial of service ("superfluous" tombstone consumption and Swift cluster slowdown) via a DELETE request with a timestamp that is older than expected.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
swiftPyPI | < 1.9.1 | 1.9.1 |
Affected products
35cpe:2.3:a:openstack:swift:*:*:*:*:*:*:*:*+ 31 more
- cpe:2.3:a:openstack:swift:*:*:*:*:*:*:*:*range: <=1.9.0
- cpe:2.3:a:openstack:swift:1.0.0:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.0.1:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.0.2:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.1.0:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.1.0:rc1:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.1.0:rc2:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.2.0:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.2.0:gamma1:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.2.0:rc1:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.3.0:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.3.0:gamma1:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.3.0:rc1:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.4.0:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.4.1:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.4.2:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.4.3:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.4.4:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.4.5:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.4.6:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.4.7:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.4.8:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.5.0:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.6.0:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.7.0:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.7.2:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.7.4:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.7.5:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.7.6:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.8.0:*:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.8.0:rc1:*:*:*:*:*:*
- cpe:2.3:a:openstack:swift:1.8.0:rc2:*:*:*:*:*:*
Patches
21f4ec235cdfdFix handling of DELETE obj reqs with old timestamp
2 files changed · +225 −27
swift/obj/server.py+34 −24 modified@@ -46,7 +46,7 @@ HTTPInternalServerError, HTTPNoContent, HTTPNotFound, HTTPNotModified, \ HTTPPreconditionFailed, HTTPRequestTimeout, HTTPUnprocessableEntity, \ HTTPClientDisconnect, HTTPMethodNotAllowed, Request, Response, UTC, \ - HTTPInsufficientStorage, multi_range_iterator + HTTPInsufficientStorage, multi_range_iterator, HTTPConflict DATADIR = 'objects' @@ -121,7 +121,6 @@ def __init__(self, path, device, partition, account, container, obj, self.tmppath = None self.logger = logger self.metadata = {} - self.meta_file = None self.data_file = None self.fp = None self.iter_etag = None @@ -133,24 +132,27 @@ def __init__(self, path, device, partition, account, container, obj, if not os.path.exists(self.datadir): return files = sorted(os.listdir(self.datadir), reverse=True) - for file in files: - if file.endswith('.ts'): - self.data_file = self.meta_file = None - self.metadata = {'deleted': True} - return - if file.endswith('.meta') and not self.meta_file: - self.meta_file = os.path.join(self.datadir, file) - if file.endswith('.data') and not self.data_file: - self.data_file = os.path.join(self.datadir, file) + meta_file = None + for afile in files: + if afile.endswith('.ts'): + self.data_file = None + with open(os.path.join(self.datadir, afile)) as mfp: + self.metadata = read_metadata(mfp) + self.metadata['deleted'] = True + break + if afile.endswith('.meta') and not meta_file: + meta_file = os.path.join(self.datadir, afile) + if afile.endswith('.data') and not self.data_file: + self.data_file = os.path.join(self.datadir, afile) break if not self.data_file: return self.fp = open(self.data_file, 'rb') self.metadata = read_metadata(self.fp) if not keep_data_fp: self.close(verify_file=False) - if self.meta_file: - with open(self.meta_file) as mfp: + if meta_file: + with open(meta_file) as mfp: for key in self.metadata.keys(): if key.lower() not in DISALLOWED_HEADERS: del self.metadata[key] @@ -594,6 +596,9 @@ def POST(self, request): except (DiskFileError, DiskFileNotExist): file.quarantine() return HTTPNotFound(request=request) + orig_timestamp = file.metadata.get('X-Timestamp', '0') + if orig_timestamp >= request.headers['x-timestamp']: + return HTTPConflict(request=request) metadata = {'X-Timestamp': request.headers['x-timestamp']} metadata.update(val for val in request.headers.iteritems() if val[0].lower().startswith('x-object-meta-')) @@ -639,6 +644,8 @@ def PUT(self, request): file = DiskFile(self.devices, device, partition, account, container, obj, self.logger, disk_chunk_size=self.disk_chunk_size) orig_timestamp = file.metadata.get('X-Timestamp') + if orig_timestamp and orig_timestamp >= request.headers['x-timestamp']: + return HTTPConflict(request=request) upload_expiration = time.time() + self.max_upload_time etag = md5() upload_size = 0 @@ -863,23 +870,26 @@ def DELETE(self, request): return HTTPPreconditionFailed( request=request, body='X-If-Delete-At and X-Delete-At do not match') - orig_timestamp = file.metadata.get('X-Timestamp') - if file.is_deleted() or file.is_expired(): - response_class = HTTPNotFound - metadata = { - 'X-Timestamp': request.headers['X-Timestamp'], 'deleted': True, - } old_delete_at = int(file.metadata.get('X-Delete-At') or 0) if old_delete_at: self.delete_at_update('DELETE', old_delete_at, account, container, obj, request.headers, device) - file.put_metadata(metadata, tombstone=True) - file.unlinkold(metadata['X-Timestamp']) - if not orig_timestamp or \ - orig_timestamp < request.headers['x-timestamp']: + orig_timestamp = file.metadata.get('X-Timestamp', 0) + req_timestamp = request.headers['X-Timestamp'] + if file.is_deleted() or file.is_expired(): + response_class = HTTPNotFound + else: + if orig_timestamp < req_timestamp: + response_class = HTTPNoContent + else: + response_class = HTTPConflict + if orig_timestamp < req_timestamp: + file.put_metadata({'X-Timestamp': req_timestamp}, + tombstone=True) + file.unlinkold(req_timestamp) self.container_update( 'DELETE', account, container, obj, request.headers, - {'x-timestamp': metadata['X-Timestamp'], + {'x-timestamp': req_timestamp, 'x-trans-id': request.headers.get('x-trans-id', '-')}, device) resp = response_class(request=request)
test/unit/obj/test_server.py+191 −3 modified@@ -509,6 +509,41 @@ def test_POST_update_meta(self): "X-Object-Meta-3" in resp.headers) self.assertEquals(resp.headers['Content-Type'], 'application/x-test') + def test_POST_old_timestamp(self): + ts = time() + timestamp = normalize_timestamp(ts) + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': timestamp, + 'Content-Type': 'application/x-test', + 'X-Object-Meta-1': 'One', + 'X-Object-Meta-Two': 'Two'}) + req.body = 'VERIFY' + resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 201) + + # Same timestamp should result in 409 + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'POST'}, + headers={'X-Timestamp': timestamp, + 'X-Object-Meta-3': 'Three', + 'X-Object-Meta-4': 'Four', + 'Content-Encoding': 'gzip', + 'Content-Type': 'application/x-test'}) + resp = self.object_controller.POST(req) + self.assertEquals(resp.status_int, 409) + + # Earlier timestamp should result in 409 + timestamp = normalize_timestamp(ts - 1) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'POST'}, + headers={'X-Timestamp': timestamp, + 'X-Object-Meta-5': 'Five', + 'X-Object-Meta-6': 'Six', + 'Content-Encoding': 'gzip', + 'Content-Type': 'application/x-test'}) + resp = self.object_controller.POST(req) + self.assertEquals(resp.status_int, 409) + def test_POST_not_exist(self): timestamp = normalize_timestamp(time()) req = Request.blank('/sda1/p/a/c/fail', @@ -555,11 +590,15 @@ def read(self, amt=None): old_http_connect = object_server.http_connect try: - timestamp = normalize_timestamp(time()) + ts = time() + timestamp = normalize_timestamp(ts) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, headers={'X-Timestamp': timestamp, 'Content-Type': 'text/plain', 'Content-Length': '0'}) resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 201) + + timestamp = normalize_timestamp(ts + 1) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, headers={'X-Timestamp': timestamp, @@ -571,6 +610,8 @@ def read(self, amt=None): object_server.http_connect = mock_http_connect(202) resp = self.object_controller.POST(req) self.assertEquals(resp.status_int, 202) + + timestamp = normalize_timestamp(ts + 2) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, headers={'X-Timestamp': timestamp, @@ -582,6 +623,8 @@ def read(self, amt=None): object_server.http_connect = mock_http_connect(202, with_exc=True) resp = self.object_controller.POST(req) self.assertEquals(resp.status_int, 202) + + timestamp = normalize_timestamp(ts + 3) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, headers={'X-Timestamp': timestamp, @@ -718,6 +761,32 @@ def test_PUT_overwrite(self): 'name': '/a/c/o', 'Content-Encoding': 'gzip'}) + def test_PUT_old_timestamp(self): + ts = time() + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': normalize_timestamp(ts), + 'Content-Length': '6', + 'Content-Type': 'application/octet-stream'}) + req.body = 'VERIFY' + resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 201) + + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': normalize_timestamp(ts), + 'Content-Type': 'text/plain', + 'Content-Encoding': 'gzip'}) + req.body = 'VERIFY TWO' + resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 409) + + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': normalize_timestamp(ts - 1), + 'Content-Type': 'text/plain', + 'Content-Encoding': 'gzip'}) + req.body = 'VERIFY THREE' + resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 409) + def test_PUT_no_etag(self): req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'X-Timestamp': normalize_timestamp(time()), @@ -1306,12 +1375,32 @@ def test_DELETE(self): self.assertEquals(resp.status_int, 400) # self.assertRaises(KeyError, self.object_controller.DELETE, req) + # The following should have created a tombstone file timestamp = normalize_timestamp(time()) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'X-Timestamp': timestamp}) resp = self.object_controller.DELETE(req) self.assertEquals(resp.status_int, 404) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assert_(os.path.isfile(objfile)) + + # The following should *not* have created a tombstone file. + timestamp = normalize_timestamp(float(timestamp) - 1) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': timestamp}) + resp = self.object_controller.DELETE(req) + self.assertEquals(resp.status_int, 404) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assertFalse(os.path.exists(objfile)) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) sleep(.00001) timestamp = normalize_timestamp(time()) @@ -1325,17 +1414,19 @@ def test_DELETE(self): resp = self.object_controller.PUT(req) self.assertEquals(resp.status_int, 201) + # The following should *not* have created a tombstone file. timestamp = normalize_timestamp(float(timestamp) - 1) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'X-Timestamp': timestamp}) resp = self.object_controller.DELETE(req) - self.assertEquals(resp.status_int, 204) + self.assertEquals(resp.status_int, 409) objfile = os.path.join(self.testdir, 'sda1', storage_directory(object_server.DATADIR, 'p', hash_path('a', 'c', 'o')), timestamp + '.ts') - self.assert_(os.path.isfile(objfile)) + self.assertFalse(os.path.exists(objfile)) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) sleep(.00001) timestamp = normalize_timestamp(time()) @@ -1350,6 +1441,103 @@ def test_DELETE(self): timestamp + '.ts') self.assert_(os.path.isfile(objfile)) + def test_DELETE_container_updates(self): + # Test swift.object_server.ObjectController.DELETE and container + # updates, making sure container update is called in the correct + # state. + timestamp = normalize_timestamp(time()) + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={ + 'X-Timestamp': timestamp, + 'Content-Type': 'application/octet-stream', + 'Content-Length': '4', + }) + req.body = 'test' + resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 201) + + calls_made = [0] + + def our_container_update(*args, **kwargs): + calls_made[0] += 1 + + orig_cu = self.object_controller.container_update + self.object_controller.container_update = our_container_update + try: + # The following request should return 409 (HTTP Conflict). A + # tombstone file should not have been created with this timestamp. + timestamp = normalize_timestamp(float(timestamp) - 1) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': timestamp}) + resp = self.object_controller.DELETE(req) + self.assertEquals(resp.status_int, 409) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assertFalse(os.path.isfile(objfile)) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) + self.assertEquals(0, calls_made[0]) + + # The following request should return 204, and the object should + # be truly deleted (container update is performed) because this + # timestamp is newer. A tombstone file should have been created + # with this timestamp. + sleep(.00001) + timestamp = normalize_timestamp(time()) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': timestamp}) + resp = self.object_controller.DELETE(req) + self.assertEquals(resp.status_int, 204) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assert_(os.path.isfile(objfile)) + self.assertEquals(1, calls_made[0]) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) + + # The following request should return a 404, as the object should + # already have been deleted, but it should have also performed a + # container update because the timestamp is newer, and a tombstone + # file should also exist with this timestamp. + sleep(.00001) + timestamp = normalize_timestamp(time()) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': timestamp}) + resp = self.object_controller.DELETE(req) + self.assertEquals(resp.status_int, 404) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assert_(os.path.isfile(objfile)) + self.assertEquals(2, calls_made[0]) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) + + # The following request should return a 404, as the object should + # already have been deleted, and it should not have performed a + # container update because the timestamp is older, or created a + # tombstone file with this timestamp. + timestamp = normalize_timestamp(float(timestamp) - 1) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': timestamp}) + resp = self.object_controller.DELETE(req) + self.assertEquals(resp.status_int, 404) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assertFalse(os.path.isfile(objfile)) + self.assertEquals(2, calls_made[0]) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) + finally: + self.object_controller.container_update = orig_cu + def test_call(self): """ Test swift.object_server.ObjectController.__call__ """ inbuf = StringIO()
6b9806e0e8cbFix handling of DELETE obj reqs with old timestamp
3 files changed · +222 −31
swift/obj/diskfile.py+10 −8 modified@@ -366,7 +366,6 @@ def __init__(self, path, device, partition, account, container, obj, self.logger = logger self.disallowed_metadata_keys = disallowed_metadata_keys or [] self.metadata = {} - self.meta_file = None self.data_file = None self.fp = None self.iter_etag = None @@ -379,13 +378,16 @@ def __init__(self, path, device, partition, account, container, obj, if not exists(self.datadir): return files = sorted(os.listdir(self.datadir), reverse=True) + meta_file = None for afile in files: if afile.endswith('.ts'): - self.data_file = self.meta_file = None - self.metadata = {'deleted': True} - return - if afile.endswith('.meta') and not self.meta_file: - self.meta_file = join(self.datadir, afile) + self.data_file = None + with open(join(self.datadir, afile)) as mfp: + self.metadata = read_metadata(mfp) + self.metadata['deleted'] = True + break + if afile.endswith('.meta') and not meta_file: + meta_file = join(self.datadir, afile) if afile.endswith('.data') and not self.data_file: self.data_file = join(self.datadir, afile) break @@ -395,8 +397,8 @@ def __init__(self, path, device, partition, account, container, obj, self.metadata = read_metadata(self.fp) if not keep_data_fp: self.close(verify_file=False) - if self.meta_file: - with open(self.meta_file) as mfp: + if meta_file: + with open(meta_file) as mfp: for key in self.metadata.keys(): if key.lower() not in self.disallowed_metadata_keys: del self.metadata[key]
swift/obj/server.py+21 −14 modified@@ -43,7 +43,8 @@ HTTPInternalServerError, HTTPNoContent, HTTPNotFound, HTTPNotModified, \ HTTPPreconditionFailed, HTTPRequestTimeout, HTTPUnprocessableEntity, \ HTTPClientDisconnect, HTTPMethodNotAllowed, Request, Response, UTC, \ - HTTPInsufficientStorage, HTTPForbidden, HTTPException, HeaderKeyDict + HTTPInsufficientStorage, HTTPForbidden, HTTPException, HeaderKeyDict, \ + HTTPConflict from swift.obj.diskfile import DiskFile, get_hashes @@ -317,6 +318,9 @@ def POST(self, request): except (DiskFileError, DiskFileNotExist): disk_file.quarantine() return HTTPNotFound(request=request) + orig_timestamp = disk_file.metadata.get('X-Timestamp', '0') + if orig_timestamp >= request.headers['x-timestamp']: + return HTTPConflict(request=request) metadata = {'X-Timestamp': request.headers['x-timestamp']} metadata.update(val for val in request.headers.iteritems() if val[0].startswith('X-Object-Meta-')) @@ -364,6 +368,8 @@ def PUT(self, request): return HTTPInsufficientStorage(drive=device, request=request) old_delete_at = int(disk_file.metadata.get('X-Delete-At') or 0) orig_timestamp = disk_file.metadata.get('X-Timestamp') + if orig_timestamp and orig_timestamp >= request.headers['x-timestamp']: + return HTTPConflict(request=request) upload_expiration = time.time() + self.max_upload_time etag = md5() elapsed_time = 0 @@ -563,25 +569,26 @@ def DELETE(self, request): return HTTPPreconditionFailed( request=request, body='X-If-Delete-At and X-Delete-At do not match') - orig_timestamp = disk_file.metadata.get('X-Timestamp') - if disk_file.is_deleted() or disk_file.is_expired(): - response_class = HTTPNotFound - else: - response_class = HTTPNoContent - metadata = { - 'X-Timestamp': request.headers['X-Timestamp'], 'deleted': True, - } old_delete_at = int(disk_file.metadata.get('X-Delete-At') or 0) if old_delete_at: self.delete_at_update('DELETE', old_delete_at, account, container, obj, request, device) - disk_file.put_metadata(metadata, tombstone=True) - disk_file.unlinkold(metadata['X-Timestamp']) - if not orig_timestamp or \ - orig_timestamp < request.headers['x-timestamp']: + orig_timestamp = disk_file.metadata.get('X-Timestamp', 0) + req_timestamp = request.headers['X-Timestamp'] + if disk_file.is_deleted() or disk_file.is_expired(): + response_class = HTTPNotFound + else: + if orig_timestamp < req_timestamp: + response_class = HTTPNoContent + else: + response_class = HTTPConflict + if orig_timestamp < req_timestamp: + disk_file.put_metadata({'X-Timestamp': req_timestamp}, + tombstone=True) + disk_file.unlinkold(req_timestamp) self.container_update( 'DELETE', account, container, obj, request, - HeaderKeyDict({'x-timestamp': metadata['X-Timestamp']}), + HeaderKeyDict({'x-timestamp': req_timestamp}), device) resp = response_class(request=request) return resp
test/unit/obj/test_server.py+191 −9 modified@@ -223,6 +223,41 @@ def test_POST_update_meta(self): "X-Object-Meta-3" in resp.headers) self.assertEquals(resp.headers['Content-Type'], 'application/x-test') + def test_POST_old_timestamp(self): + ts = time() + timestamp = normalize_timestamp(ts) + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': timestamp, + 'Content-Type': 'application/x-test', + 'X-Object-Meta-1': 'One', + 'X-Object-Meta-Two': 'Two'}) + req.body = 'VERIFY' + resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 201) + + # Same timestamp should result in 409 + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'POST'}, + headers={'X-Timestamp': timestamp, + 'X-Object-Meta-3': 'Three', + 'X-Object-Meta-4': 'Four', + 'Content-Encoding': 'gzip', + 'Content-Type': 'application/x-test'}) + resp = self.object_controller.POST(req) + self.assertEquals(resp.status_int, 409) + + # Earlier timestamp should result in 409 + timestamp = normalize_timestamp(ts - 1) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'POST'}, + headers={'X-Timestamp': timestamp, + 'X-Object-Meta-5': 'Five', + 'X-Object-Meta-6': 'Six', + 'Content-Encoding': 'gzip', + 'Content-Type': 'application/x-test'}) + resp = self.object_controller.POST(req) + self.assertEquals(resp.status_int, 409) + def test_POST_not_exist(self): timestamp = normalize_timestamp(time()) req = Request.blank('/sda1/p/a/c/fail', @@ -269,14 +304,16 @@ def read(self, amt=None): old_http_connect = object_server.http_connect try: - timestamp = normalize_timestamp(time()) + ts = time() + timestamp = normalize_timestamp(ts) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, headers={'X-Timestamp': timestamp, 'Content-Type': 'text/plain', 'Content-Length': '0'}) resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 201) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Timestamp': timestamp, + headers={'X-Timestamp': normalize_timestamp(ts + 1), 'X-Container-Host': '1.2.3.4:0', 'X-Container-Partition': '3', 'X-Container-Device': 'sda1', @@ -287,7 +324,7 @@ def read(self, amt=None): self.assertEquals(resp.status_int, 202) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Timestamp': timestamp, + headers={'X-Timestamp': normalize_timestamp(ts + 2), 'X-Container-Host': '1.2.3.4:0', 'X-Container-Partition': '3', 'X-Container-Device': 'sda1', @@ -298,7 +335,7 @@ def read(self, amt=None): self.assertEquals(resp.status_int, 202) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'POST'}, - headers={'X-Timestamp': timestamp, + headers={'X-Timestamp': normalize_timestamp(ts + 3), 'X-Container-Host': '1.2.3.4:0', 'X-Container-Partition': '3', 'X-Container-Device': 'sda1', @@ -439,6 +476,32 @@ def test_PUT_overwrite(self): 'name': '/a/c/o', 'Content-Encoding': 'gzip'}) + def test_PUT_old_timestamp(self): + ts = time() + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': normalize_timestamp(ts), + 'Content-Length': '6', + 'Content-Type': 'application/octet-stream'}) + req.body = 'VERIFY' + resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 201) + + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': normalize_timestamp(ts), + 'Content-Type': 'text/plain', + 'Content-Encoding': 'gzip'}) + req.body = 'VERIFY TWO' + resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 409) + + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={'X-Timestamp': normalize_timestamp(ts - 1), + 'Content-Type': 'text/plain', + 'Content-Encoding': 'gzip'}) + req.body = 'VERIFY THREE' + resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 409) + def test_PUT_no_etag(self): req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, headers={'X-Timestamp': normalize_timestamp(time()), @@ -1014,7 +1077,7 @@ def test_GET_quarantine_range(self): self.assertEquals(resp.status_int, 404) def test_DELETE(self): - """ Test swift.object_server.ObjectController.DELETE """ + # Test swift.object_server.ObjectController.DELETE req = Request.blank('/sda1/p/a/c', environ={'REQUEST_METHOD': 'DELETE'}) resp = self.object_controller.DELETE(req) @@ -1026,12 +1089,32 @@ def test_DELETE(self): self.assertEquals(resp.status_int, 400) # self.assertRaises(KeyError, self.object_controller.DELETE, req) + # The following should have created a tombstone file timestamp = normalize_timestamp(time()) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'X-Timestamp': timestamp}) resp = self.object_controller.DELETE(req) self.assertEquals(resp.status_int, 404) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assert_(os.path.isfile(objfile)) + + # The following should *not* have created a tombstone file. + timestamp = normalize_timestamp(float(timestamp) - 1) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': timestamp}) + resp = self.object_controller.DELETE(req) + self.assertEquals(resp.status_int, 404) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assertFalse(os.path.isfile(objfile)) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) sleep(.00001) timestamp = normalize_timestamp(time()) @@ -1045,17 +1128,19 @@ def test_DELETE(self): resp = self.object_controller.PUT(req) self.assertEquals(resp.status_int, 201) + # The following should *not* have created a tombstone file. timestamp = normalize_timestamp(float(timestamp) - 1) req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'DELETE'}, headers={'X-Timestamp': timestamp}) resp = self.object_controller.DELETE(req) - self.assertEquals(resp.status_int, 204) + self.assertEquals(resp.status_int, 409) objfile = os.path.join(self.testdir, 'sda1', storage_directory(object_server.DATADIR, 'p', hash_path('a', 'c', 'o')), timestamp + '.ts') - self.assert_(os.path.isfile(objfile)) + self.assertFalse(os.path.isfile(objfile)) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) sleep(.00001) timestamp = normalize_timestamp(time()) @@ -1070,6 +1155,103 @@ def test_DELETE(self): timestamp + '.ts') self.assert_(os.path.isfile(objfile)) + def test_DELETE_container_updates(self): + # Test swift.object_server.ObjectController.DELETE and container + # updates, making sure container update is called in the correct + # state. + timestamp = normalize_timestamp(time()) + req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, + headers={ + 'X-Timestamp': timestamp, + 'Content-Type': 'application/octet-stream', + 'Content-Length': '4', + }) + req.body = 'test' + resp = self.object_controller.PUT(req) + self.assertEquals(resp.status_int, 201) + + calls_made = [0] + + def our_container_update(*args, **kwargs): + calls_made[0] += 1 + + orig_cu = self.object_controller.container_update + self.object_controller.container_update = our_container_update + try: + # The following request should return 409 (HTTP Conflict). A + # tombstone file should not have been created with this timestamp. + timestamp = normalize_timestamp(float(timestamp) - 1) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': timestamp}) + resp = self.object_controller.DELETE(req) + self.assertEquals(resp.status_int, 409) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assertFalse(os.path.isfile(objfile)) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) + self.assertEquals(0, calls_made[0]) + + # The following request should return 204, and the object should + # be truly deleted (container update is performed) because this + # timestamp is newer. A tombstone file should have been created + # with this timestamp. + sleep(.00001) + timestamp = normalize_timestamp(time()) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': timestamp}) + resp = self.object_controller.DELETE(req) + self.assertEquals(resp.status_int, 204) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assert_(os.path.isfile(objfile)) + self.assertEquals(1, calls_made[0]) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) + + # The following request should return a 404, as the object should + # already have been deleted, but it should have also performed a + # container update because the timestamp is newer, and a tombstone + # file should also exist with this timestamp. + sleep(.00001) + timestamp = normalize_timestamp(time()) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': timestamp}) + resp = self.object_controller.DELETE(req) + self.assertEquals(resp.status_int, 404) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assert_(os.path.isfile(objfile)) + self.assertEquals(2, calls_made[0]) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) + + # The following request should return a 404, as the object should + # already have been deleted, and it should not have performed a + # container update because the timestamp is older, or created a + # tombstone file with this timestamp. + timestamp = normalize_timestamp(float(timestamp) - 1) + req = Request.blank('/sda1/p/a/c/o', + environ={'REQUEST_METHOD': 'DELETE'}, + headers={'X-Timestamp': timestamp}) + resp = self.object_controller.DELETE(req) + self.assertEquals(resp.status_int, 404) + objfile = os.path.join(self.testdir, 'sda1', + storage_directory(object_server.DATADIR, 'p', + hash_path('a', 'c', 'o')), + timestamp + '.ts') + self.assertFalse(os.path.isfile(objfile)) + self.assertEquals(2, calls_made[0]) + self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1) + finally: + self.object_controller.container_update = orig_cu + def test_call(self): """ Test swift.object_server.ObjectController.__call__ """ inbuf = StringIO() @@ -1161,7 +1343,7 @@ def my_storage_directory(*args): 'SERVER_PROTOCOL': 'HTTP/1.0', 'CONTENT_LENGTH': '0', 'CONTENT_TYPE': 'text/html', - 'HTTP_X_TIMESTAMP': 1.2, + 'HTTP_X_TIMESTAMP': '1.2', 'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': inbuf, @@ -1184,7 +1366,7 @@ def my_storage_directory(*args): 'SERVER_PROTOCOL': 'HTTP/1.0', 'CONTENT_LENGTH': '0', 'CONTENT_TYPE': 'text/html', - 'HTTP_X_TIMESTAMP': 1.3, + 'HTTP_X_TIMESTAMP': '1.3', 'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': inbuf,
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
15- github.com/advisories/GHSA-wxx2-gqvv-34hxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2013-4155ghsaADVISORY
- rhn.redhat.com/errata/RHSA-2013-1197.htmlnvdWEB
- www.debian.org/security/2012/dsa-2737nvdWEB
- www.openwall.com/lists/oss-security/2013/08/07/6nvdWEB
- www.ubuntu.com/usn/USN-2001-1nvdWEB
- bugs.launchpad.net/swift/+bug/1196932nvdWEB
- github.com/openstack/swift/commit/1f4ec235cdfd8c868f2d6458532f9dc32c00b8caghsaWEB
- github.com/openstack/swift/commit/6b9806e0e8cbec60c0a3ece0bd516e0502827515ghsaWEB
- review.openstack.orgghsaWEB
- review.openstack.orgghsaWEB
- review.openstack.orgghsaWEB
- review.openstack.orgnvd
- review.openstack.orgnvd
- review.openstack.orgnvd
News mentions
0No linked articles in our index yet.