Skip to content
This repository was archived by the owner on Sep 26, 2019. It is now read-only.

Commit cd094ee

Browse files
committed
Stop using client headers for cross-middleware communication
Previously, we would use client-accessible headers to pass the S3 access key, signature, and normalized request to authentication middleware. Specifically, we would send the following headers: Authorization: AWS <access key>:<signature> X-Auth-Token: <base64-encoded normalized request> However, few authentication middleware would validate that the Authorization header actually started with "AWS ", the only prefix that Swift3 would actually handle. As a result, the authentication middlewares had no way to validate that the normalized request came from swift3 rather than the client itself. This leads to a security hole wherein an attacker who has captured a single valid request through the S3 API or who has obtained a valid pre-signed URL may impersonate the user that issued the request or pre-signed URL indefinitely through the Swift API. Now, the S3 authentication information will be placed in a separate namespace in the WSGI environment, completely inaccessible to the client. Specifically, environ['swift3.auth_details'] = { 'access_key': <access key>, 'signature': <signature>, 'string_to_sign': <normalized request>, } (Note that the normalized request is no longer base64-encoded.) UpgradeImpact This is a breaking API change. No currently-deployed authentication middlewares will work with this. This patch includes a fix for s3_token (used to authenticate against Keystone); any deployers still using keystonemiddleware to provide s3_token should switch to using swift3. Similar changes are being proposed for Swauth and tempauth. Proprietary authentication middlewares will need to be updated to use the new environment keys as well. When upgrading Swift3, operators will need to upgrade their Swift3-capable authentication middleware at the same time. Closes-Bug: 1561199 Change-Id: Ia3fbb4938f0daa8845cba4137a01cc43bc1a713c Depends-On: Ib90adcc2f059adaf203fba1c95b2154561ea7487
1 parent 74d818f commit cd094ee

File tree

6 files changed

+186
-95
lines changed

6 files changed

+186
-95
lines changed

swift3/request.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
import base64
1716
from email.header import Header
1817
from hashlib import sha256, md5
1918
import re
@@ -386,7 +385,12 @@ def __init__(self, env, app=None, slo_enabled=True):
386385
self.bucket_in_host = self._parse_host()
387386
self.container_name, self.object_name = self._parse_uri()
388387
self._validate_headers()
389-
self.token = base64.urlsafe_b64encode(self._string_to_sign())
388+
self.environ['swift3.auth_details'] = {
389+
'access_key': self.access_key,
390+
'signature': signature,
391+
'string_to_sign': self._string_to_sign(),
392+
}
393+
self.token = None
390394
self.account = None
391395
self.user_id = None
392396
self.slo_enabled = slo_enabled
@@ -1199,13 +1203,15 @@ def authenticate(self, app):
11991203
sw_resp.environ['HTTP_X_USER_NAME'])
12001204
self.user_id = utf8encode(self.user_id)
12011205
self.token = sw_resp.environ.get('HTTP_X_AUTH_TOKEN')
1202-
# Need to skip S3 authorization since authtoken middleware
1203-
# overwrites account in PATH_INFO
1204-
del self.headers['Authorization']
12051206
else:
12061207
# tempauth
12071208
self.user_id = self.access_key
12081209

1210+
# Need to skip S3 authorization on subsequent requests to prevent
1211+
# overwriting the account in PATH_INFO
1212+
del self.headers['Authorization']
1213+
del self.environ['swift3.auth_details']
1214+
12091215
def to_swift_req(self, method, container, obj, query=None,
12101216
body=None, headers=None):
12111217
sw_req = super(S3AclRequest, self).to_swift_req(

swift3/s3_token_middleware.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
3232
"""
3333

34+
import base64
3435
import json
3536
import logging
3637

@@ -176,31 +177,24 @@ def __call__(self, environ, start_response):
176177
return self._app(environ, start_response)
177178

178179
# Read request signature and access id.
179-
if 'Authorization' not in req.headers:
180-
msg = 'No Authorization header. skipping.'
180+
s3_auth_details = req.environ.get('swift3.auth_details')
181+
if not s3_auth_details:
182+
msg = 'No authorization deatils from Swift3. skipping.'
181183
self._logger.debug(msg)
182184
return self._app(environ, start_response)
183185

184-
token = req.headers.get('X-Auth-Token',
185-
req.headers.get('X-Storage-Token'))
186-
if not token:
187-
msg = 'You did not specify an auth or a storage token. skipping.'
188-
self._logger.debug(msg)
189-
return self._app(environ, start_response)
186+
access = s3_auth_details['access_key']
187+
if isinstance(access, six.binary_type):
188+
access = access.decode('utf-8')
190189

191-
auth_header = req.headers['Authorization']
192-
try:
193-
access, signature = auth_header.split(' ')[-1].rsplit(':', 1)
194-
except ValueError:
195-
if self._delay_auth_decision:
196-
self._logger.debug('Invalid Authorization header: %s - '
197-
'deferring reject downstream', auth_header)
198-
return self._app(environ, start_response)
199-
else:
200-
self._logger.debug('Invalid Authorization header: %s - '
201-
'rejecting request', auth_header)
202-
return self._deny_request('InvalidURI')(
203-
environ, start_response)
190+
signature = s3_auth_details['signature']
191+
if isinstance(signature, six.binary_type):
192+
signature = signature.decode('utf-8')
193+
194+
string_to_sign = s3_auth_details['string_to_sign']
195+
if isinstance(string_to_sign, six.text_type):
196+
string_to_sign = string_to_sign.encode('utf-8')
197+
token = base64.urlsafe_b64encode(string_to_sign).encode('ascii')
204198

205199
# NOTE(chmou): This is to handle the special case with nova
206200
# when we have the option s3_affix_tenant. We will force it to

swift3/test/unit/test_middleware.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from contextlib import nested
1919
from datetime import datetime
2020
import hashlib
21-
import base64
2221
import requests
2322
import json
2423
import copy
@@ -102,16 +101,21 @@ def canonical_string(path, headers):
102101
path, query_string = path.split('?', 1)
103102
else:
104103
query_string = ''
104+
env = {
105+
'REQUEST_METHOD': 'GET',
106+
'PATH_INFO': path,
107+
'QUERY_STRING': query_string,
108+
'HTTP_AUTHORIZATION': 'AWS X:Y:Z',
109+
}
110+
for header, value in headers.items():
111+
header = 'HTTP_' + header.replace('-', '_').upper()
112+
if header in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
113+
header = header[5:]
114+
env[header] = value
105115

106116
with patch('swift3.request.Request._validate_headers'):
107-
req = S3Request({
108-
'REQUEST_METHOD': 'GET',
109-
'PATH_INFO': path,
110-
'QUERY_STRING': query_string,
111-
'HTTP_AUTHORIZATION': 'AWS X:Y:Z',
112-
})
113-
req.headers.update(headers)
114-
return req._string_to_sign()
117+
req = S3Request(env)
118+
return req.environ['swift3.auth_details']['string_to_sign']
115119

116120
def verify(hash, path, headers):
117121
s = canonical_string(path, headers)
@@ -379,10 +383,12 @@ def test_token_generation(self):
379383
req.headers['Date'] = date_header
380384
status, headers, body = self.call_swift3(req)
381385
_, _, headers = self.swift.calls_with_headers[-1]
382-
self.assertEqual(base64.urlsafe_b64decode(
383-
headers['X-Auth-Token']),
384-
'PUT\n\n\n%s\n/bucket/object?partNumber=1&uploadId=123456789abcdef'
385-
% date_header)
386+
self.assertEqual(req.environ['swift3.auth_details'], {
387+
'access_key': 'test:tester',
388+
'signature': 'hmac',
389+
'string_to_sign': '\n'.join([
390+
'PUT', '', '', date_header,
391+
'/bucket/object?partNumber=1&uploadId=123456789abcdef'])})
386392

387393
def test_invalid_uri(self):
388394
req = Request.blank('/bucket/invalid\xffname',

swift3/test/unit/test_obj.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,26 +131,51 @@ def test_object_HEAD_error(self):
131131
status, headers, body = self.call_swift3(req)
132132
self.assertEqual(status.split()[0], '403')
133133
self.assertEqual(body, '') # sanity
134+
135+
req = Request.blank('/bucket/object',
136+
environ={'REQUEST_METHOD': 'HEAD'},
137+
headers={'Authorization': 'AWS test:tester:hmac',
138+
'Date': self.get_date_header()})
134139
self.swift.register('HEAD', '/v1/AUTH_test/bucket/object',
135140
swob.HTTPForbidden, {}, None)
136141
status, headers, body = self.call_swift3(req)
137142
self.assertEqual(status.split()[0], '403')
138143
self.assertEqual(body, '') # sanity
144+
145+
req = Request.blank('/bucket/object',
146+
environ={'REQUEST_METHOD': 'HEAD'},
147+
headers={'Authorization': 'AWS test:tester:hmac',
148+
'Date': self.get_date_header()})
139149
self.swift.register('HEAD', '/v1/AUTH_test/bucket/object',
140150
swob.HTTPNotFound, {}, None)
141151
status, headers, body = self.call_swift3(req)
142152
self.assertEqual(status.split()[0], '404')
143153
self.assertEqual(body, '') # sanity
154+
155+
req = Request.blank('/bucket/object',
156+
environ={'REQUEST_METHOD': 'HEAD'},
157+
headers={'Authorization': 'AWS test:tester:hmac',
158+
'Date': self.get_date_header()})
144159
self.swift.register('HEAD', '/v1/AUTH_test/bucket/object',
145160
swob.HTTPPreconditionFailed, {}, None)
146161
status, headers, body = self.call_swift3(req)
147162
self.assertEqual(status.split()[0], '412')
148163
self.assertEqual(body, '') # sanity
164+
165+
req = Request.blank('/bucket/object',
166+
environ={'REQUEST_METHOD': 'HEAD'},
167+
headers={'Authorization': 'AWS test:tester:hmac',
168+
'Date': self.get_date_header()})
149169
self.swift.register('HEAD', '/v1/AUTH_test/bucket/object',
150170
swob.HTTPServerError, {}, None)
151171
status, headers, body = self.call_swift3(req)
152172
self.assertEqual(status.split()[0], '500')
153173
self.assertEqual(body, '') # sanity
174+
175+
req = Request.blank('/bucket/object',
176+
environ={'REQUEST_METHOD': 'HEAD'},
177+
headers={'Authorization': 'AWS test:tester:hmac',
178+
'Date': self.get_date_header()})
154179
self.swift.register('HEAD', '/v1/AUTH_test/bucket/object',
155180
swob.HTTPServiceUnavailable, {}, None)
156181
status, headers, body = self.call_swift3(req)

swift3/test/unit/test_request.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def create_s3request_with_param(param, value):
221221
self.assertEqual(
222222
result.exception.headers['content-type'], 'application/xml')
223223

224-
def test_authenticate_delete_Authorization_from_s3req_headers(self):
224+
def test_authenticate_delete_Authorization_from_s3req(self):
225225
req = Request.blank('/bucket/obj',
226226
environ={'REQUEST_METHOD': 'GET'},
227227
headers={'Authorization': 'AWS test:tester:hmac',
@@ -232,11 +232,12 @@ def test_authenticate_delete_Authorization_from_s3req_headers(self):
232232

233233
m_swift_resp.return_value = FakeSwiftResponse()
234234
s3_req = S3AclRequest(req.environ, MagicMock())
235-
self.assertTrue('HTTP_AUTHORIZATION' not in s3_req.environ)
236-
self.assertTrue('Authorization' not in s3_req.headers)
235+
self.assertNotIn('swift3.auth_details', s3_req.environ)
236+
self.assertNotIn('HTTP_AUTHORIZATION', s3_req.environ)
237+
self.assertNotIn('Authorization', s3_req.headers)
237238
self.assertEqual(s3_req.token, 'token')
238239

239-
def test_to_swift_req_Authorization_not_exist_in_swreq_headers(self):
240+
def test_to_swift_req_Authorization_not_exist_in_swreq(self):
240241
container = 'bucket'
241242
obj = 'obj'
242243
method = 'GET'
@@ -251,6 +252,7 @@ def test_to_swift_req_Authorization_not_exist_in_swreq_headers(self):
251252
m_swift_resp.return_value = FakeSwiftResponse()
252253
s3_req = S3AclRequest(req.environ, MagicMock())
253254
sw_req = s3_req.to_swift_req(method, container, obj)
255+
self.assertNotIn('swift3.auth_details', sw_req.environ)
254256
self.assertNotIn('HTTP_AUTHORIZATION', sw_req.environ)
255257
self.assertNotIn('Authorization', sw_req.headers)
256258
self.assertEqual(sw_req.headers['X-Auth-Token'], 'token')

0 commit comments

Comments
 (0)