CVE-2012-3426
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
KeystonePyPI | < 8.0.0a0 | 8.0.0a0 |
Affected products
4Patches
6375838cfceb8Carrying over token expiry time when token chaining
2 files changed · +14 −1
keystone/service.py+2 −1 modified@@ -351,7 +351,8 @@ def authenticate(self, context, auth=None): 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
tests/test_keystoneclient.py+12 −0 modified@@ -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 @@ def test_disable_user_invalidates_token(self): 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
29e74e73a6e5Carrying over token expiry time when token chaining
2 files changed · +14 −1
keystone/service.py+2 −1 modified@@ -351,7 +351,8 @@ def authenticate(self, context, auth=None): 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
tests/test_keystoneclient.py+12 −0 modified@@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +import time import uuid import nose.exc @@ -326,6 +327,17 @@ def test_disable_user_invalidates_token(self): 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
d9600434da14Invalidate user tokens when a user is disabled
3 files changed · +44 −13
keystone/identity/core.py+12 −10 modified@@ -408,6 +408,17 @@ def update_user(self, context, user_id, user): 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 @@ def set_user_enabled(self, context, user_id, user): 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."""
keystone/service.py+13 −1 modified@@ -28,6 +28,9 @@ from keystone.common import wsgi +LOG = logging.getLogger(__name__) + + class AdminRouter(wsgi.ComposingRouter): def __init__(self): mapper = routes.Mapper() @@ -275,7 +278,8 @@ def authenticate(self, context, auth=None): # 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 @@ def authenticate(self, context, auth=None): 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:
tests/test_keystoneclient.py+19 −2 modified@@ -309,6 +309,23 @@ def test_change_password_invalidates_token(self): 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 @@ def test_user_create_update_delete(self): 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') @@ -871,7 +888,7 @@ def test_user_create_update_delete(self): 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')
628149b3dc6bInvalidate user tokens when a user is disabled
3 files changed · +44 −13
keystone/identity/core.py+12 −10 modified@@ -408,6 +408,17 @@ def update_user(self, context, user_id, user): 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 @@ def set_user_enabled(self, context, user_id, user): 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."""
keystone/service.py+13 −1 modified@@ -28,6 +28,9 @@ from keystone.common import wsgi +LOG = logging.getLogger(__name__) + + class AdminRouter(wsgi.ComposingRouter): def __init__(self): mapper = routes.Mapper() @@ -275,7 +278,8 @@ def authenticate(self, context, auth=None): # 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 @@ def authenticate(self, context, auth=None): 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:
tests/test_keystoneclient.py+19 −2 modified@@ -309,6 +309,23 @@ def test_change_password_invalidates_token(self): 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 @@ def test_user_create_update_delete(self): 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 @@ def test_user_create_update_delete(self): 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')
ea03d05ed5deInvalidate user tokens when password is changed
6 files changed · +76 −1
AUTHORS+1 −0 modified@@ -25,6 +25,7 @@ Darren Birkett <darren.birkett@gmail.com> dcramer <david.cramer@rackspace.com> Dean Troyer <dtroyer@gmail.com> Deepak Garg <deepakgarg.iitg@gmail.com> +Derek Higgins <derekh@redhat.com> Devin Carlen <devin.carlen@gmail.com> Dolph Mathews <dolph.mathews@gmail.com> Dolph Mathews <dolph.mathews@rackspace.com>
keystone/identity/core.py+13 −1 modified@@ -24,12 +24,15 @@ 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 @@ def set_user_enabled(self, context, user_id, user): 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."""
keystone/token/backends/kvs.py+15 −0 modified@@ -44,3 +44,18 @@ def delete_token(self, token_id): 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
keystone/token/backends/sql.py+14 −0 modified@@ -81,3 +81,17 @@ def delete_token(self, token_id): 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
keystone/token/core.py+10 −0 modified@@ -87,6 +87,16 @@ def delete_token(self, token_id): """ 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.
tests/test_keystoneclient.py+23 −0 modified@@ -286,6 +286,29 @@ def test_invalid_user_password(self): 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
a67b24878a61Invalidate user tokens when password is changed
5 files changed · +75 −1
keystone/identity/core.py+13 −1 modified@@ -24,12 +24,15 @@ 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 @@ def set_user_enabled(self, context, user_id, user): 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."""
keystone/token/backends/kvs.py+15 −0 modified@@ -44,3 +44,18 @@ def delete_token(self, token_id): 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
keystone/token/backends/sql.py+14 −0 modified@@ -81,3 +81,17 @@ def delete_token(self, token_id): 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
keystone/token/core.py+10 −0 modified@@ -87,6 +87,16 @@ def delete_token(self, token_id): """ 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.
tests/test_keystoneclient.py+23 −0 modified@@ -286,6 +286,29 @@ def test_invalid_user_password(self): 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
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
20- github.com/openstack/keystone/commit/375838cfceb88cacc312ff6564e64eb18ee6a355nvdPatchWEB
- www.openwall.com/lists/oss-security/2012/07/27/4nvdPatchWEB
- launchpad.net/keystone/essex/2012.1.1/+download/keystone-2012.1.1.tar.gznvdPatchWEB
- github.com/openstack/keystone/commit/628149b3dc6b58b91fd08e6ca8d91c728ccb8626nvdExploitPatchWEB
- github.com/openstack/keystone/commit/ea03d05ed5de0c015042876100d37a6a14bf56denvdExploitPatchWEB
- github.com/advisories/GHSA-xp97-6w7r-4cjcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2012-3426ghsaADVISORY
- github.com/openstack/keystone/commit/29e74e73a6e51cffc0371b32354558391826a4aanvdWEB
- github.com/openstack/keystone/commit/a67b24878a6156eab17b9098fa649f0279256f5dnvdWEB
- github.com/openstack/keystone/commit/d9600434da14976463a0bd03abd8e0309f0db454nvdWEB
- www.ubuntu.com/usn/USN-1552-1nvdWEB
- bugs.launchpad.net/keystone/+bug/996595nvdWEB
- bugs.launchpad.net/keystone/+bug/997194nvdWEB
- bugs.launchpad.net/keystone/+bug/998185nvdWEB
- github.com/openstack/keystone/commit/375838cfceb88cacc312ff6564e64eb18ee6a355ghsaWEB
- github.com/openstack/keystone/commit/628149b3dc6b58b91fd08e6ca8d91c728ccb8626ghsaWEB
- github.com/openstack/keystone/commit/a67b24878a6156eab17b9098fa649f0279256f5dghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/keystone/PYSEC-2012-34.yamlghsaWEB
- secunia.com/advisories/50045nvd
- secunia.com/advisories/50494nvd
News mentions
0No linked articles in our index yet.