From 3a8f5dbf9c49fdf1cf2d0b7ba35b82f25f88e634 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Tue, 11 Dec 2018 15:29:35 -0800 Subject: Verify client input for v4 signatures Previously, we would use the X-Amz-Content-SHA256 value when calculating signatures, but wouldn't actually check the content that was sent. This would allow a malicious third party that managed to capture the headers for an object upload to overwrite that with arbitrary content provided they could do so within the 5-minute clock-skew window. Now, we wrap the wsgi.input that's sent on to the proxy-server app to hash content as it's read and raise an error if there's a mismatch. Note that clients using presigned-urls to upload have no defense against a similar replay attack. Notwithstanding the above security consideration, this *also* provides better assurances that the client's payload was received correctly. Note that this *does not* attempt to send an etag in footers, however, so the proxy-to-object-server connection is not guarded against bit-flips. In the future, Swift will hopefully grow a way to perform SHA256 verification on the object-server. This would offer two main benefits: - End-to-end message integrity checking. - Move CPU load of calculating the hash from the proxy (which is somewhat CPU-bound) to the object-server (which tends to have CPU to spare). Change-Id: I61eb12455c37376be4d739eee55a5f439216f0e9 Closes-Bug: 1765834 --- swift/common/middleware/s3api/s3request.py | 60 ++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 7 deletions(-) (limited to 'swift/common/middleware/s3api/s3request.py') diff --git a/swift/common/middleware/s3api/s3request.py b/swift/common/middleware/s3api/s3request.py index d796475c9..68eeaa8f4 100644 --- a/swift/common/middleware/s3api/s3request.py +++ b/swift/common/middleware/s3api/s3request.py @@ -24,7 +24,8 @@ import six from six.moves.urllib.parse import quote, unquote, parse_qsl import string -from swift.common.utils import split_path, json, get_swift_info +from swift.common.utils import split_path, json, get_swift_info, \ + close_if_possible from swift.common import swob from swift.common.http import HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, \ HTTP_NO_CONTENT, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, \ @@ -110,6 +111,34 @@ def _header_acl_property(resource): doc='Get and set the %s acl property' % resource) +class HashingInput(object): + """ + wsgi.input wrapper to verify the hash of the input as it's read. + """ + def __init__(self, reader, content_length, hasher, expected_hex_hash): + self._input = reader + self._to_read = content_length + self._hasher = hasher() + self._expected = expected_hex_hash + + def read(self, size=None): + chunk = self._input.read(size) + self._hasher.update(chunk) + self._to_read -= len(chunk) + if self._to_read < 0 or (size > len(chunk) and self._to_read) or ( + self._to_read == 0 and + self._hasher.hexdigest() != self._expected): + self.close() + # Since we don't return the last chunk, the PUT never completes + raise swob.HTTPUnprocessableEntity( + 'The X-Amz-Content-SHA56 you specified did not match ' + 'what we received.') + return chunk + + def close(self): + close_if_possible(self._input) + + class SigV4Mixin(object): """ A request class mixin to provide S3 signature v4 functionality @@ -401,6 +430,20 @@ class SigV4Mixin(object): raise InvalidRequest(msg) else: hashed_payload = self.headers['X-Amz-Content-SHA256'] + if self.content_length == 0: + if hashed_payload != sha256().hexdigest(): + raise BadDigest( + 'The X-Amz-Content-SHA56 you specified did not match ' + 'what we received.') + elif self.content_length: + self.environ['wsgi.input'] = HashingInput( + self.environ['wsgi.input'], + self.content_length, + sha256, + hashed_payload) + # else, not provided -- Swift will kick out a 411 Length Required + # which will get translated back to a S3-style response in + # S3Request._swift_error_codes cr.append(hashed_payload) return '\n'.join(cr).encode('utf-8') @@ -1264,12 +1307,15 @@ class S3Request(swob.Request): sw_req = self.to_swift_req(method, container, obj, headers=headers, body=body, query=query) - sw_resp = sw_req.get_response(app) - - # reuse account and tokens - _, self.account, _ = split_path(sw_resp.environ['PATH_INFO'], - 2, 3, True) - self.account = utf8encode(self.account) + try: + sw_resp = sw_req.get_response(app) + except swob.HTTPException as err: + sw_resp = err + else: + # reuse account and tokens + _, self.account, _ = split_path(sw_resp.environ['PATH_INFO'], + 2, 3, True) + self.account = utf8encode(self.account) resp = S3Response.from_swift_resp(sw_resp) status = resp.status_int # pylint: disable-msg=E1101 -- cgit v1.2.1