Hash token before storing it in Swift

Swauth uses token value as object name. Object names are logged in proxy
and object servers. Anybody with access to proxy/object server logs can
see token values. Attacker can use this token to access user's data in
Swift store. Instead of token, hashed token (with HASH_PATH_PREFIX and
HASH_PATH_SUFFIX) is used as object name now.

WARNING: In deployments without memcached this patch logs out all users
because tokens became invalid.

CVE-2017-16613

SecurityImpact
Closes-Bug: #1655781
Change-Id: I0d01e8e95400c82ef25f98e2d269532e83233c2c
This commit is contained in:
Pavel Kvasnicka 2017-11-21 09:38:09 +01:00 committed by Pavel Kvasnička
parent 54ac16ad67
commit 70af798626
3 changed files with 59 additions and 5 deletions

View File

@ -15,6 +15,7 @@
import base64
from hashlib import sha1
from hashlib import sha512
import hmac
from httplib import HTTPConnection
from httplib import HTTPSConnection
@ -50,6 +51,8 @@ from swift.common.middleware.acl import referrer_allowed
from swift.common.utils import cache_from_env
from swift.common.utils import get_logger
from swift.common.utils import get_remote_client
from swift.common.utils import HASH_PATH_PREFIX
from swift.common.utils import HASH_PATH_SUFFIX
from swift.common.utils import split_path
from swift.common.utils import TRUE_VALUES
from swift.common.utils import urlparse
@ -289,6 +292,15 @@ class Swauth(object):
env['swift.clean_acl'] = clean_acl
return self.app(env, start_response)
def _get_concealed_token(self, token):
"""Returns hashed token to be used as object name in Swift.
Tokens are stored in auth account but object names are visible in Swift
logs. Object names are hashed from token.
"""
enc_key = "%s:%s:%s" % (HASH_PATH_PREFIX, token, HASH_PATH_SUFFIX)
return sha512(enc_key).hexdigest()
def get_groups(self, env, token):
"""Get groups for the given token.
@ -397,8 +409,9 @@ class Swauth(object):
memcache_key, (time() + expires_from_now, groups),
time=expires_from_now)
else:
object_name = self._get_concealed_token(token)
path = quote('/v1/%s/.token_%s/%s' %
(self.auth_account, token[-1], token))
(self.auth_account, object_name[-1], object_name))
resp = self.make_pre_authed_request(
env, 'GET', path).get_response(self.app)
if resp.status_int // 100 != 2:
@ -1168,8 +1181,9 @@ class Swauth(object):
(path, resp.status))
candidate_token = resp.headers.get('x-object-meta-auth-token')
if candidate_token:
object_name = self._get_concealed_token(candidate_token)
path = quote('/v1/%s/.token_%s/%s' %
(self.auth_account, candidate_token[-1], candidate_token))
(self.auth_account, object_name[-1], object_name))
resp = self.make_pre_authed_request(
req.environ, 'DELETE', path).get_response(self.app)
if resp.status_int // 100 != 2 and resp.status_int != 404:
@ -1318,8 +1332,9 @@ class Swauth(object):
expires = None
candidate_token = resp.headers.get('x-object-meta-auth-token')
if candidate_token:
object_name = self._get_concealed_token(candidate_token)
path = quote('/v1/%s/.token_%s/%s' %
(self.auth_account, candidate_token[-1], candidate_token))
(self.auth_account, object_name[-1], object_name))
delete_token = False
try:
if req.headers.get('x-auth-new-token', 'false').lower() in \
@ -1362,8 +1377,9 @@ class Swauth(object):
# Generate new token
token = '%stk%s' % (self.reseller_prefix, uuid4().hex)
# Save token info
object_name = self._get_concealed_token(token)
path = quote('/v1/%s/.token_%s/%s' %
(self.auth_account, token[-1], token))
(self.auth_account, object_name[-1], object_name))
try:
token_life = min(
int(req.headers.get('x-auth-token-lifetime',
@ -1439,8 +1455,9 @@ class Swauth(object):
if expires < time():
groups = None
if not groups:
object_name = self._get_concealed_token(token)
path = quote('/v1/%s/.token_%s/%s' %
(self.auth_account, token[-1], token))
(self.auth_account, object_name[-1], object_name))
resp = self.make_pre_authed_request(
req.environ, 'GET', path).get_response(self.app)
if resp.status_int // 100 != 2:

View File

@ -202,5 +202,6 @@ class TestSha512(unittest.TestCase):
match = self.auth_encoder.match('keystring2', creds, **creds_dict)
self.assertEqual(match, False)
if __name__ == '__main__':
unittest.main()

View File

@ -4125,6 +4125,42 @@ class TestAuth(unittest.TestCase):
# Assert that string passed to hmac.new is only the hash
self.assertEqual(mock_hmac_new.call_args[0][0], key_hash)
def test_get_concealed_token(self):
auth.HASH_PATH_PREFIX = 'start'
auth.HASH_PATH_SUFFIX = 'end'
token = 'token'
# Check sha512 of "start:token:end"
hashed_token = self.test_auth._get_concealed_token(token)
self.assertEqual(hashed_token,
'cb320540b0b4c69eb83de2ffb80714cb6766e2d06b5579d1a35a9c4c3fb62'
'981ec50bcc3fb94521133e69a87d1efcb83efd78f35a06b6375e410201476'
'0722f6')
# Check sha512 of "start:token2:end"
token = 'token2'
hashed_token = self.test_auth._get_concealed_token(token)
self.assertEqual(hashed_token,
'ca400a6f884c168357f6af0609fda66aecd5aa613147167487495dd9f39fd'
'8a77288568e65857294f01e398d7f14328e855f18517ccf94185d849e7f34'
'f4259d')
# Check sha512 of "start2:token2:end"
auth.HASH_PATH_PREFIX = 'start2'
hashed_token = self.test_auth._get_concealed_token(token)
self.assertEqual(hashed_token,
'ad594a69f44dd6e0aad54e360b01f15bd4833ccb4dcd9116d7aba0c25fb95'
'670155b8cc7175def7aeeb4624a0f2bb7da5f0b204a4680ea7947d3d6a045'
'22bdde')
# Check sha512 of "start2:token2:end2"
auth.HASH_PATH_SUFFIX = 'end2'
hashed_token = self.test_auth._get_concealed_token(token)
self.assertEqual(hashed_token,
'446af2473ad6b28319a0fe02719a9d715b9941d12e0709851aedb4f53b890'
'693e7f1328e68d870fe114f35f4ed9648b16a5013182db50d3d1f79a660f2'
'0e078e')
if __name__ == '__main__':
unittest.main()