diff --git a/py3-constraints.txt b/py3-constraints.txt index 6271838c0d..e1464ea680 100644 --- a/py3-constraints.txt +++ b/py3-constraints.txt @@ -17,10 +17,10 @@ autopage===0.5.2 bandit===1.7.10;python_version>='3.8' bandit===1.7.5;python_version=='3.7' bandit===1.7.1;python_version=='3.6' -boto3===1.35.71;python_version>='3.8' +boto3===1.36.6;python_version>='3.8' boto3===1.33.13;python_version=='3.7' boto3===1.23.10;python_version=='3.6' -botocore===1.35.71;python_version>='3.8' +botocore===1.36.6;python_version>='3.8' botocore===1.33.13;python_version=='3.7' botocore===1.26.10;python_version=='3.6' certifi===2024.8.30 @@ -188,7 +188,7 @@ rfc3986===2.0.0;python_version>='3.7' rfc3986===1.5.0;python_version=='3.6' rich===13.9.3;python_version>='3.8' rich===13.8.1;python_version=='3.7' -s3transfer===0.10.4;python_version>='3.8' +s3transfer===0.11.2;python_version>='3.8' s3transfer===0.8.2;python_version=='3.7' s3transfer===0.5.2;python_version=='3.6' setuptools===75.3.0;python_version>='3.12' diff --git a/swift/common/middleware/s3api/controllers/multi_delete.py b/swift/common/middleware/s3api/controllers/multi_delete.py index 17cbb62e98..65cdfc3f72 100644 --- a/swift/common/middleware/s3api/controllers/multi_delete.py +++ b/swift/common/middleware/s3api/controllers/multi_delete.py @@ -77,7 +77,12 @@ class MultiObjectDeleteController(Controller): if not xml: raise MissingRequestBodyError() - req.check_md5(xml) + if 'x-amz-content-sha256' not in req.headers: + # SHA256 got checked when we read the body, so there's at + # least *some* verification. Recent versions of boto3 stopped + # sending Content-MD5, so it can't *always* be required. + # See https://bugs.launchpad.net/swift/+bug/2098529 + req.check_md5(xml) elem = fromstring(xml, 'Delete', self.logger) quiet = elem.find('./Quiet') diff --git a/test/unit/common/middleware/s3api/test_multi_delete.py b/test/unit/common/middleware/s3api/test_multi_delete.py index 8cf4e5d282..b4c415ad22 100644 --- a/test/unit/common/middleware/s3api/test_multi_delete.py +++ b/test/unit/common/middleware/s3api/test_multi_delete.py @@ -15,6 +15,7 @@ # limitations under the License. import base64 +import hashlib import json import unittest from datetime import datetime @@ -62,6 +63,87 @@ class BaseS3ApiMultiDelete(object): status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '200') + def test_object_multi_DELETE_no_content_md5(self): + elem = Element('Delete') + obj = SubElement(elem, 'Object') + SubElement(obj, 'Key').text = 'object' + body = tostring(elem, use_s3ns=False) + + req = Request.blank('/bucket/object?delete', + environ={'REQUEST_METHOD': 'POST'}, + headers={'Authorization': 'AWS test:tester:hmac', + 'Date': self.get_date_header(), + }, + body=body) + + status, headers, body = self.call_s3api(req) + self.assertEqual(status.split()[0], '400') + self.assertEqual(self._get_error_code(body), 'InvalidRequest') + self.assertIn(b'Missing required header', body) + self.assertIn(b'Content-MD5', body) + + def test_object_multi_DELETE_sha256_invalid(self): + elem = Element('Delete') + obj = SubElement(elem, 'Object') + SubElement(obj, 'Key').text = 'object' + body = tostring(elem, use_s3ns=False) + content_sha256 = 'invalid' + + req = Request.blank('/bucket/object?delete', + environ={'REQUEST_METHOD': 'POST'}, + headers={'Authorization': 'AWS test:tester:hmac', + 'Date': self.get_date_header(), + 'X-Amz-Content-SHA256': content_sha256, + }, + body=body) + + status, headers, body = self.call_s3api(req) + self.assertEqual(status.split()[0], '400') + self.assertEqual(self._get_error_code(body), + 'XAmzContentSHA256Mismatch') + self.assertIn(b"provided 'x-amz-content-sha256' header " + b"does not match", body) + + def test_object_multi_DELETE_sha256_bad(self): + elem = Element('Delete') + obj = SubElement(elem, 'Object') + SubElement(obj, 'Key').text = 'object' + body = tostring(elem, use_s3ns=False) + content_sha256 = hashlib.sha256(body[:-1]).hexdigest() + + req = Request.blank('/bucket/object?delete', + environ={'REQUEST_METHOD': 'POST'}, + headers={'Authorization': 'AWS test:tester:hmac', + 'Date': self.get_date_header(), + 'X-Amz-Content-SHA256': content_sha256, + }, + body=body) + + status, headers, body = self.call_s3api(req) + self.assertEqual(status.split()[0], '400') + self.assertEqual(self._get_error_code(body), + 'XAmzContentSHA256Mismatch') + self.assertIn(b"provided 'x-amz-content-sha256' header " + b"does not match", body) + + def test_object_multi_DELETE_sha256_valid(self): + elem = Element('Delete') + obj = SubElement(elem, 'Object') + SubElement(obj, 'Key').text = 'object' + body = tostring(elem, use_s3ns=False) + content_sha256 = hashlib.sha256(body).hexdigest() + + req = Request.blank('/bucket/object?delete', + environ={'REQUEST_METHOD': 'POST'}, + headers={'Authorization': 'AWS test:tester:hmac', + 'Date': self.get_date_header(), + 'X-Amz-Content-SHA256': content_sha256, + }, + body=body) + + status, headers, body = self.call_s3api(req) + self.assertEqual(status.split()[0], '200') + def test_object_multi_DELETE(self): self.swift.register('DELETE', '/v1/AUTH_test/bucket/Key1', swob.HTTPNoContent, {}, None)