From 90b34140929bafabaed5776548912a48aa3e45cb Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Fri, 20 Apr 2018 19:34:29 +0000 Subject: [PATCH] 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 claculating the hash from the proxy (which is somewhat CPU-bound) to the object-server (which tends to have CPU to spare). Change-Id: Ibe92bffaf9ea5e8e0a1148afed9def6430aac2e9 --- swift3/request.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/swift3/request.py b/swift3/request.py index 7a2936e..16241cf 100644 --- a/swift3/request.py +++ b/swift3/request.py @@ -14,6 +14,7 @@ # limitations under the License. import base64 +import binascii from collections import defaultdict from email.header import Header from hashlib import sha1, sha256, md5 @@ -108,6 +109,32 @@ 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_hash): + self._input = reader + self._to_read = content_length + self._hasher = hasher() + self._expected = expected_hash + + def read(self, size): + chunk = self._input.read(size) + self._hasher.update(chunk) + self._to_read -= len(chunk) + if self._to_read < 0 or (self._to_read == 0 and + self._hasher.digest() != self._expected): + raise swob.HTTPUnprocessableEntity( + 'The X-Amz-Content-SHA56 you specified did not match ' + 'what we received.') + return chunk + + def close(self): + if hasattr(self._input, 'close'): + self._input.close() + + class SigV4Mixin(object): """ A request class mixin to provide S3 signature v4 functionality @@ -356,6 +383,18 @@ 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, + binascii.unhexlify(hashed_payload)) + # else, not provided -- we send back a 411... somehwere... cr.append(hashed_payload) return '\n'.join(cr).encode('utf-8') -- 1.9.1