summaryrefslogtreecommitdiff
path: root/swift/common/middleware/s3api/s3request.py
diff options
context:
space:
mode:
authorTim Burke <tim.burke@gmail.com>2018-12-11 15:29:35 -0800
committerTim Burke <tim.burke@gmail.com>2019-01-08 19:54:12 +0000
commit3a8f5dbf9c49fdf1cf2d0b7ba35b82f25f88e634 (patch)
tree28547028f9814976e55d6e3c6f643b78bff4a7ea /swift/common/middleware/s3api/s3request.py
parent7b1679b9b28dde9588af4f6cee04d2de734a91fa (diff)
downloadswift-3a8f5dbf9c49fdf1cf2d0b7ba35b82f25f88e634.tar.gz
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
Diffstat (limited to 'swift/common/middleware/s3api/s3request.py')
-rw-r--r--swift/common/middleware/s3api/s3request.py60
1 files changed, 53 insertions, 7 deletions
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