CVE-2017-16613 (published 2017-11-21T13:29Z)

An issue was discovered in middleware.py in OpenStack Swauth through 1.2.0 when used with OpenStack Swift through 2.15.1. The Swift object store and proxy server are saving (unhashed) tokens retrieved from the Swauth middleware authentication mechanism to a log file as part of a GET URI. This allows attackers to bypass authentication by inserting a token into an X-Auth-Token header of a new request. NOTE: github.com/openstack/swauth URLs do not mean that Swauth is maintained by an official OpenStack project team.

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 9.8, CRITICAL (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-287
cwe_name: Improper Authentication
description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
extended_description: 
            
			
         
         

Fix information:
repo_url: https://github.com/openstack/swauth
commit_details: repo_url: https://github.com/openstack/swauth
author: Pavel Kvasnicka
author_date: 2017-11-21 09:38:09+01:00
msg: 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
repository: repo_url: https://github.com/openstack/swauth
repo_name: openstack-archive/swauth
description: MOVED: now at https://opendev.org/x/swauth
owner: openstack
file_changes: filename: middleware.py
change_type: ModificationType.MODIFY
diff: @@ -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:

programming_language: Python
filename: test_authtypes.py
change_type: ModificationType.MODIFY
diff: @@ -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()

programming_language: Python
filename: test_middleware.py
change_type: ModificationType.MODIFY
diff: @@ -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()

programming_language: Python