CVE-2012-3426 (published 2012-07-31T10:45Z)

OpenStack Keystone before 2012.1.1, as used in OpenStack Folsom before Folsom-1 and OpenStack Essex, does not properly implement token expiration, which allows remote authenticated users to bypass intended authorization restrictions by (1) creating new tokens through token chaining, (2) leveraging possession of a token for a disabled user account, or (3) leveraging possession of a token for an account with a changed password.

Severity: MEDIUM
CVSS v2 base score: 4.9 (AV:N/AC:M/Au:S/C:P/I:P/A:N)

Weakness information:
cwe_id: CWE-264
cwe_name: Permissions, Privileges, and Access Controls
description: Weaknesses in this category are related to the management of permissions, privileges, and other security features that are used to perform access control.

Fix information:
repo_url: https://github.com/openstack/keystone
commit_details: repo_url: https://github.com/openstack/keystone
author: Derek Higgins
author_date: 2012-05-11 13:42:43+01:00
msg: Invalidate user tokens when a user is disabled

Fixes Bug 997194

Delete valid tokens for a user when they have been disabled

Moved logic to delete tokens into update_user, as this can be called
directly form the REST API.

Also checks if a user is enabled when creating a token from another
token, this helps in cases there the backend didn't support listing of
tokens (and as a result weren't deleted)

Change-Id: Ib5ed73a7873bfa66ef31bf6d0f0322f50e677688
repository: repo_url: https://github.com/openstack/keystone
repo_name: openstack/keystone
description: OpenStack Identity (Keystone). Mirror of code maintained at opendev.org.
repo_language: Python
owner: openstack
file_changes: filename: core.py
change_type: ModificationType.MODIFY
diff: @@ -408,6 +408,17 @@ class UserController(wsgi.Application):
             raise exception.UserNotFound(user_id=user_id)
 
         user_ref = self.identity_api.update_user(context, user_id, user)
+
+        # If the password was changed or the user was disabled we clear tokens
+        if user.get('password') or user.get('enabled', True) == False:
+            try:
+                for token_id in self.token_api.list_tokens(context, user_id):
+                    self.token_api.delete_token(context, token_id)
+            except exception.NotImplemented:
+                # The users status has been changed but tokens remain valid for
+                # backends that can't list tokens for users
+                LOG.warning('User %s status has changed, but existing tokens '
+                            'remain valid' % user_id)
         return {'user': user_ref}
 
     def delete_user(self, context, user_id):
@@ -421,16 +432,7 @@ class UserController(wsgi.Application):
         return self.update_user(context, user_id, user)
 
     def set_user_password(self, context, user_id, user):
-        user_ref = self.update_user(context, user_id, user)
-        try:
-            for token_id in self.token_api.list_tokens(context, user_id):
-                self.token_api.delete_token(context, token_id)
-        except exception.NotImplemented:
-            # The password has been changed but tokens remain valid for
-            # backends that can't list tokens for users
-            LOG.warning('Password changed for %s, but existing tokens remain '
-                        'valid' % user_id)
-        return user_ref
+        return self.update_user(context, user_id, user)
 
     def update_user_tenant(self, context, user_id, user):
         """Update the default tenant."""

programming_language: Python
filename: service.py
change_type: ModificationType.MODIFY
diff: @@ -28,6 +28,9 @@ from keystone.common import utils
 from keystone.common import wsgi
 
 
+LOG = logging.getLogger(__name__)
+
+
 class AdminRouter(wsgi.ComposingRouter):
     def __init__(self):
         mapper = routes.Mapper()
@@ -275,7 +278,8 @@ class TokenController(wsgi.Application):
 
                 # If the user is disabled don't allow them to authenticate
                 if not user_ref.get('enabled', True):
-                    raise exception.Forbidden(message='User has been disabled')
+                    LOG.warning('User %s is disabled' % user_id)
+                    raise exception.Unauthorized()
             except AssertionError as e:
                 raise exception.Unauthorized(e.message)
 
@@ -314,6 +318,14 @@ class TokenController(wsgi.Application):
 
             user_ref = old_token_ref['user']
 
+            # If the user is disabled don't allow them to authenticate
+            current_user_ref = self.identity_api.get_user(
+                                                    context=context,
+                                                    user_id=user_ref['id'])
+            if not current_user_ref.get('enabled', True):
+                LOG.warning('User %s is disabled' % user_ref['id'])
+                raise exception.Unauthorized()
+
             tenants = self.identity_api.get_tenants_for_user(context,
                                                              user_ref['id'])
             if tenant_id:

programming_language: Python
filename: test_keystoneclient.py
change_type: ModificationType.MODIFY
diff: @@ -309,6 +309,23 @@ class KeystoneClientTests(object):
                           client.tokens.authenticate,
                           token=token_id)
 
+    def test_disable_user_invalidates_token(self):
+        from keystoneclient import exceptions as client_exceptions
+
+        admin_client = self.get_client(admin=True)
+        foo_client = self.get_client(self.user_foo)
+
+        admin_client.users.update_enabled(user=self.user_foo['id'],
+                                          enabled=False)
+
+        self.assertRaises(client_exceptions.Unauthorized,
+                          foo_client.tokens.authenticate,
+                          token=foo_client.auth_token)
+
+        self.assertRaises(client_exceptions.Unauthorized,
+                          self.get_client,
+                          self.user_foo)
+
     def test_user_create_update_delete(self):
         from keystoneclient import exceptions as client_exceptions
 
@@ -332,7 +349,7 @@ class KeystoneClientTests(object):
         user = client.users.get(user.id)
         self.assertFalse(user.enabled)
 
-        self.assertRaises(client_exceptions.AuthorizationFailure,
+        self.assertRaises(client_exceptions.Unauthorized,
                   self._client,
                   username=test_username,
                   password='password')
@@ -880,7 +897,7 @@ class KcEssex3TestCase(CompatTestCase, KeystoneClientTests):
         user = client.users.get(user.id)
         self.assertFalse(user.enabled)
 
-        self.assertRaises(client_exceptions.AuthorizationFailure,
+        self.assertRaises(client_exceptions.Unauthorized,
                   self._client,
                   username=test_username,
                   password='password')

programming_language: Python
repo_url: https://github.com/openstack/keystone
commit_details: repo_url: https://github.com/openstack/keystone
author: Derek Higgins
author_date: 2012-06-05 09:33:53+01:00
msg: Carrying over token expiry time when token chaining

Fixes bug #998185

This commit causes the token expiry time to be maintained when
one token is being created from another

Change-Id: I7b61692a60d9227423b93c267864a5abe939ca33
repository: repo_url: https://github.com/openstack/keystone
repo_name: openstack/keystone
description: OpenStack Identity (Keystone). Mirror of code maintained at opendev.org.
repo_language: Python
owner: openstack
file_changes: filename: service.py
change_type: ModificationType.MODIFY
diff: @@ -351,7 +351,8 @@ class TokenController(wsgi.Application):
                     context, token_id, dict(id=token_id,
                                             user=user_ref,
                                             tenant=tenant_ref,
-                                            metadata=metadata_ref))
+                                            metadata=metadata_ref,
+                                            expires=old_token_ref['expires']))
 
         # TODO(termie): optimize this call at some point and put it into the
         #               the return for metadata

programming_language: Python
filename: test_keystoneclient.py
change_type: ModificationType.MODIFY
diff: @@ -14,6 +14,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import time
 import uuid
 
 import nose.exc
@@ -333,6 +334,17 @@ class KeystoneClientTests(object):
                           self.get_client,
                           self.user_foo)
 
+    def test_token_expiry_maintained(self):
+        foo_client = self.get_client(self.user_foo)
+        orig_token = foo_client.service_catalog.catalog['token']
+
+        time.sleep(1.01)
+        reauthenticated_token = foo_client.tokens.authenticate(
+                                    token=foo_client.auth_token)
+
+        self.assertEquals(orig_token['expires'],
+                          reauthenticated_token.expires)
+
     def test_user_create_update_delete(self):
         from keystoneclient import exceptions as client_exceptions
 

programming_language: Python
repo_url: https://github.com/openstack/keystone
commit_details: repo_url: https://github.com/openstack/keystone
author: Derek Higgins
author_date: 2012-05-09 15:55:46+01:00
msg: Invalidate user tokens when password is changed

Fixes bug 996595

This commit will cause all valid tokens to be deleted for a user
who's password is changed (implemented for the sql and kvs backends)

Change-Id: I6ad7da8957b7041983a3fc91d9ba9368667d06ac
repository: repo_url: https://github.com/openstack/keystone
repo_name: openstack/keystone
description: OpenStack Identity (Keystone). Mirror of code maintained at opendev.org.
repo_language: Python
owner: openstack
file_changes: filename: core.py
change_type: ModificationType.MODIFY
diff: @@ -24,12 +24,15 @@ from keystone import config
 from keystone import exception
 from keystone import policy
 from keystone import token
+from keystone.common import logging
 from keystone.common import manager
 from keystone.common import wsgi
 
 
 CONF = config.CONF
 
+LOG = logging.getLogger(__name__)
+
 
 class Manager(manager.Manager):
     """Default pivot point for the Identity backend.
@@ -418,7 +421,16 @@ class UserController(wsgi.Application):
         return self.update_user(context, user_id, user)
 
     def set_user_password(self, context, user_id, user):
-        return self.update_user(context, user_id, user)
+        user_ref = self.update_user(context, user_id, user)
+        try:
+            for token_id in self.token_api.list_tokens(context, user_id):
+                self.token_api.delete_token(context, token_id)
+        except exception.NotImplemented:
+            # The password has been changed but tokens remain valid for
+            # backends that can't list tokens for users
+            LOG.warning('Password changed for %s, but existing tokens remain '
+                        'valid' % user_id)
+        return user_ref
 
     def update_user_tenant(self, context, user_id, user):
         """Update the default tenant."""

programming_language: Python
filename: kvs.py
change_type: ModificationType.MODIFY
diff: @@ -44,3 +44,18 @@ class Token(kvs.Base, token.Driver):
             return self.db.delete('token-%s' % token_id)
         except KeyError:
             raise exception.TokenNotFound(token_id=token_id)
+
+    def list_tokens(self, user_id):
+        tokens = []
+        now = datetime.datetime.utcnow()
+        for token, user_ref in self.db.items():
+            if not token.startswith('token-'):
+                continue
+            if 'user' not in user_ref:
+                continue
+            if user_ref['user'].get('id') != user_id:
+                continue
+            if user_ref.get('expires') and user_ref.get('expires') < now:
+                continue
+            tokens.append(token.split('-', 1)[1])
+        return tokens

programming_language: Python
filename: sql.py
change_type: ModificationType.MODIFY
diff: @@ -81,3 +81,17 @@ class Token(sql.Base, token.Driver):
         with session.begin():
             session.delete(token_ref)
             session.flush()
+
+    def list_tokens(self, user_id):
+        session = self.get_session()
+        tokens = []
+        now = datetime.datetime.utcnow()
+        for token_ref in session.query(TokenModel)\
+                                      .filter(TokenModel.expires > now):
+            token_ref_dict = token_ref.to_dict()
+            if 'user' not in token_ref_dict:
+                continue
+            if token_ref_dict['user'].get('id') != user_id:
+                continue
+            tokens.append(token_ref['id'])
+        return tokens

programming_language: Python
filename: core.py
change_type: ModificationType.MODIFY
diff: @@ -87,6 +87,16 @@ class Driver(object):
         """
         raise exception.NotImplemented()
 
+    def list_tokens(self, user_id):
+        """Returns a list of current token_id's for a user
+
+        :param user_id: identity of the user
+        :type user_id: string
+        :returns: list of token_id's
+
+        """
+        raise exception.NotImplemented()
+
     def _get_default_expire_time(self):
         """Determine when a token should expire based on the config.
 

programming_language: Python
filename: test_keystoneclient.py
change_type: ModificationType.MODIFY
diff: @@ -286,6 +286,29 @@ class KeystoneClientTests(object):
                           username='blah',
                           password='blah')
 
+    def test_change_password_invalidates_token(self):
+        from keystoneclient import exceptions as client_exceptions
+
+        client = self.get_client(admin=True)
+
+        username = uuid.uuid4().hex
+        passwd = uuid.uuid4().hex
+        user = client.users.create(name=username, password=passwd,
+                                   email=uuid.uuid4().hex)
+
+        token_id = client.tokens.authenticate(username=username,
+                                              password=passwd).id
+
+        # authenticate with a token should work before a password change
+        client.tokens.authenticate(token=token_id)
+
+        client.users.update_password(user=user.id, password=uuid.uuid4().hex)
+
+        # authenticate with a token should not work after a password change
+        self.assertRaises(client_exceptions.Unauthorized,
+                          client.tokens.authenticate,
+                          token=token_id)
+
     def test_user_create_update_delete(self):
         from keystoneclient import exceptions as client_exceptions
 

programming_language: Python