CVE-2017-16613: Improper Authentication (CWE-287)
value: 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.
CWE description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
CVSS v3 base score: 9.8
Published: 2017-11-21T13:29Z
Repository: https://github.com/openstack/swauth
Language: Python
Files changed: swauth/middleware.py, test/unit/test_authtypes.py, test/unit/test_middleware.py
Commit date: 2017-11-21 12:01:22 +0100
Commit message: 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

Diff: diff --git a/swauth/middleware.py b/swauth/middleware.py
index 6f5f8fe..94be7e0 100644
--- a/swauth/middleware.py
+++ b/swauth/middleware.py
@@ -13,10 +13,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
 import base64
 from hashlib import sha1
+from hashlib import sha512
 import hmac
 from httplib import HTTPConnection
 from httplib import HTTPSConnection
 import json
 import swift
@@ -48,10 +49,12 @@ from swift.common.middleware.acl import clean_acl
 from swift.common.middleware.acl import parse_acl
 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
 import swift.common.wsgi
 
@@ -287,10 +290,19 @@ class Swauth(object):
             elif 'swift.authorize' not in env:
                 env['swift.authorize'] = self.authorize
                 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.
 
         :param env: The current WSGI environment dictionary.
         :param token: Token to validate and return a group string for.
@@ -395,12 +407,13 @@ class Swauth(object):
                 if memcache_client:
                     memcache_client.set(
                         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:
                     return None
                 detail = json.loads(resp.body)
@@ -1166,12 +1179,13 @@ class Swauth(object):
         elif resp.status_int // 100 != 2:
             raise Exception('Could not obtain user details: %s %s' %
                             (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:
                 raise Exception('Could not delete possibly existing token: '
                                 '%s %s' % (path, resp.status))
@@ -1316,12 +1330,13 @@ class Swauth(object):
         # See if a token already exists and hasn't expired
         token = None
         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 \
                         TRUE_VALUES:
                     delete_token = True
@@ -1360,12 +1375,13 @@ class Swauth(object):
             account_id = \
                 resp.headers['x-container-meta-account-id']
             # 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',
                                         self.token_life)),
                     self.max_token_life)
@@ -1437,12 +1453,13 @@ class Swauth(object):
             if cached_auth_data:
                 expires, groups = cached_auth_data
                 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:
                 return HTTPNotFound(request=req)
             detail = json.loads(resp.body)
diff --git a/test/unit/test_authtypes.py b/test/unit/test_authtypes.py
index 91d67b9..669af90 100644
--- a/test/unit/test_authtypes.py
+++ b/test/unit/test_authtypes.py
@@ -200,7 +200,8 @@ class TestSha512(unittest.TestCase):
                                '367b7daa3f0e8f3dcfe798e426b9e332776c8da59c0c11'
                                'd4832931d1bf48830f670ecc6ceb04fbad0f')
         match = self.auth_encoder.match('keystring2', creds, **creds_dict)
         self.assertEqual(match, False)
 
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/test/unit/test_middleware.py b/test/unit/test_middleware.py
index 26bd26a..b4afcb0 100644
--- a/test/unit/test_middleware.py
+++ b/test/unit/test_middleware.py
@@ -4123,8 +4123,44 @@ class TestAuth(unittest.TestCase):
             self.test_auth.get_groups(env, token)
         self.assertTrue(mock_hmac_new.called)
         # 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()

Vulnerable code:                              (self.auth_account, token[-1], token))
                (self.auth_account, candidate_token[-1], candidate_token))
                (self.auth_account, candidate_token[-1], candidate_token))
                         (self.auth_account, token[-1], token))
                         (self.auth_account, token[-1], token))
Fixed code: from hashlib import sha512
from swift.common.utils import HASH_PATH_PREFIX
from swift.common.utils import HASH_PATH_SUFFIX
    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()

                object_name = self._get_concealed_token(token)
                             (self.auth_account, object_name[-1], object_name))
            object_name = self._get_concealed_token(candidate_token)
                (self.auth_account, object_name[-1], object_name))
            object_name = self._get_concealed_token(candidate_token)
                (self.auth_account, object_name[-1], object_name))
            object_name = self._get_concealed_token(token)
                         (self.auth_account, object_name[-1], object_name))
            object_name = self._get_concealed_token(token)
                         (self.auth_account, object_name[-1], object_name))

    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')