commit 90afb9ab8a560d43ce02975151b38e50f1f227ca Author: Clay Gerrard Date: Mon Dec 12 14:58:11 2022 -0600 s3api: Prevent XXE injections Previously, clients could use XML external entities (XXEs) to read arbitrary files from proxy-servers and inject the content into the request. Since many S3 APIs reflect request content back to the user, this could be used to extract any secrets that the swift user could read, such as tempauth credentials, keymaster secrets, etc. Now, disable entity resolution -- any unknown entities will be replaced with an empty string. Without resolving the entities, the request is still processed. But a warning is logged when there are entities in the XML. Change-Id: I8f7370834bc2b717b6141f8e88b81a02821053b7 Closes-Bug: #1998625 Co-Authored-By: Romain de Joux diff --git a/swift/common/middleware/s3api/etree.py b/swift/common/middleware/s3api/etree.py index 987b84a14..e16b75340 100644 --- a/swift/common/middleware/s3api/etree.py +++ b/swift/common/middleware/s3api/etree.py @@ -130,7 +130,7 @@ class _Element(lxml.etree.ElementBase): parser_lookup = lxml.etree.ElementDefaultClassLookup(element=_Element) -parser = lxml.etree.XMLParser() +parser = lxml.etree.XMLParser(resolve_entities=False, no_network=True) parser.set_element_class_lookup(parser_lookup) Element = parser.makeelement diff --git a/test/unit/common/middleware/s3api/test_multi_delete.py b/test/unit/common/middleware/s3api/test_multi_delete.py index 0cfe438de..04fc63efe 100644 --- a/test/unit/common/middleware/s3api/test_multi_delete.py +++ b/test/unit/common/middleware/s3api/test_multi_delete.py @@ -523,6 +523,7 @@ class TestS3ApiMultiDelete(S3ApiTestCase): body=body) status, headers, body = self.call_s3api(req) self.assertEqual(status.split()[0], '200') + self.assertIn(b'Key1Server Error', body) def _test_object_multi_DELETE(self, account): self.keys = ['Key1', 'Key2'] @@ -580,6 +581,43 @@ class TestS3ApiMultiDelete(S3ApiTestCase): elem = fromstring(body) self.assertEqual(len(elem.findall('Deleted')), len(self.keys)) + def test_object_multi_DELETE_with_system_entity(self): + self.keys = ['Key1', 'Key2'] + self.swift.register( + 'DELETE', '/v1/AUTH_test/bucket/%s' % self.keys[0], + swob.HTTPNotFound, {}, None) + self.swift.register( + 'DELETE', '/v1/AUTH_test/bucket/%s' % self.keys[1], + swob.HTTPNoContent, {}, None) + + elem = Element('Delete') + for key in self.keys: + obj = SubElement(elem, 'Object') + SubElement(obj, 'Key').text = key + body = tostring(elem, use_s3ns=False) + body = body.replace( + b'?>\n', + b'?>\n ]>\n', + ).replace(b'>Key1<', b'>Key1&ent;<') + content_md5 = ( + base64.b64encode(md5(body, usedforsecurity=False).digest()) + .strip()) + + req = Request.blank('/bucket?delete', + environ={'REQUEST_METHOD': 'POST'}, + headers={'Authorization': 'AWS test:full_control:hmac', + 'Date': self.get_date_header(), + 'Content-MD5': content_md5}, + body=body) + req.date = datetime.now() + req.content_type = 'text/plain' + + status, headers, body = self.call_s3api(req) + self.assertEqual(status, '200 OK', body) + self.assertIn(b'Key2', body) + self.assertNotIn(b'root:/root', body) + self.assertIn(b'Key1', body) + def _test_no_body(self, use_content_length=False, use_transfer_encoding=False, string_to_md5=b''): content_md5 = (base64.b64encode(