High severityNVD Advisory· Published Aug 25, 2014· Updated May 6, 2026
CVE-2014-5251
CVE-2014-5251
Description
The MySQL token driver in OpenStack Identity (Keystone) 2014.1.x before 2014.1.2.1 and Juno before Juno-3 stores timestamps with the incorrect precision, which causes the expiration comparison for tokens to fail and allows remote authenticated users to retain access via an expired token.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
keystonePyPI | < 8.0.0a0 | 8.0.0a0 |
Affected products
5- cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*
Patches
26cbf835542d6Fix revocation event handling with MySQL
4 files changed · +31 −18
keystone/contrib/revoke/model.py+22 −6 modified@@ -69,6 +69,11 @@ def __init__(self, **kwargs): # This is revoking all tokens for a domain. self.domain_scope_id = None + if self.expires_at is not None: + # Trim off the expiration time because MySQL timestamps are only + # accurate to the second. + self.expires_at = self.expires_at.replace(microsecond=0) + if self.revoked_at is None: self.revoked_at = timeutils.utcnow() if self.issued_before is None: @@ -90,8 +95,7 @@ def to_dict(self): if self.consumer_id is not None: event['OS-OAUTH1:access_token_id'] = self.access_token_id if self.expires_at is not None: - event['expires_at'] = timeutils.isotime(self.expires_at, - subsecond=True) + event['expires_at'] = timeutils.isotime(self.expires_at) if self.issued_before is not None: event['issued_before'] = timeutils.isotime(self.issued_before, subsecond=True) @@ -242,9 +246,15 @@ def is_revoked(self, token_data): def build_token_values_v2(access, default_domain_id): token_data = access['token'] + + token_expires_at = timeutils.parse_isotime(token_data['expires']) + + # Trim off the microseconds because the revocation event only has + # expirations accurate to the second. + token_expires_at = token_expires_at.replace(microsecond=0) + token_values = { - 'expires_at': timeutils.normalize_time( - timeutils.parse_isotime(token_data['expires'])), + 'expires_at': timeutils.normalize_time(token_expires_at), 'issued_at': timeutils.normalize_time( timeutils.parse_isotime(token_data['issued_at']))} @@ -282,9 +292,15 @@ def build_token_values_v2(access, default_domain_id): def build_token_values(token_data): + + token_expires_at = timeutils.parse_isotime(token_data['expires_at']) + + # Trim off the microseconds because the revocation event only has + # expirations accurate to the second. + token_expires_at = token_expires_at.replace(microsecond=0) + token_values = { - 'expires_at': timeutils.normalize_time( - timeutils.parse_isotime(token_data['expires_at'])), + 'expires_at': timeutils.normalize_time(token_expires_at), 'issued_at': timeutils.normalize_time( timeutils.parse_isotime(token_data['issued_at']))}
keystone/tests/test_revoke.py+3 −3 modified@@ -350,7 +350,7 @@ def test_by_user_expiration(self): event = self._revoke_by_expiration(user_id, future_time) token_data_1 = _sample_blank_token() token_data_1['user_id'] = user_id - token_data_1['expires_at'] = future_time + token_data_1['expires_at'] = future_time.replace(microsecond=0) self._assertTokenRevoked(token_data_1) token_data_2 = _sample_blank_token() @@ -375,7 +375,7 @@ def test_by_user_project(self): token_data = _sample_blank_token() token_data['user_id'] = user_id token_data['project_id'] = project_id - token_data['expires_at'] = future_time + token_data['expires_at'] = future_time.replace(microsecond=0) self._revoke_by_expiration(user_id, future_time, project_id=project_id) self._assertTokenRevoked(token_data) @@ -392,7 +392,7 @@ def test_by_user_domain(self): token_data = _sample_blank_token() token_data['user_id'] = user_id token_data['assignment_domain_id'] = domain_id - token_data['expires_at'] = future_time + token_data['expires_at'] = future_time.replace(microsecond=0) self._revoke_by_expiration(user_id, future_time, domain_id=domain_id) self._assertTokenRevoked(token_data)
keystone/tests/test_v3_auth.py+5 −7 modified@@ -1441,6 +1441,11 @@ def test_disable_domain_shows_in_event_list(self): def assertUserAndExpiryInList(self, events, user_id, expires_at): found = False for e in events: + + # Timestamps in the event list are accurate to second. + expires_at = timeutils.parse_isotime(expires_at) + expires_at = timeutils.isotime(expires_at) + if e['user_id'] == user_id and e['expires_at'] == expires_at: found = True self.assertTrue(found, @@ -1464,14 +1469,9 @@ def test_list_delete_token_shows_in_event_list(self): response.json_body['token'] headers3 = {'X-Subject-Token': response.headers['X-Subject-Token']} - scoped_token = self.get_scoped_token() - headers_unrevoked = {'X-Subject-Token': scoped_token} - self.head('/auth/tokens', headers=headers, expected_status=200) self.head('/auth/tokens', headers=headers2, expected_status=200) self.head('/auth/tokens', headers=headers3, expected_status=200) - self.head('/auth/tokens', headers=headers_unrevoked, - expected_status=200) self.delete('/auth/tokens', headers=headers, expected_status=204) # NOTE(ayoung): not deleting token3, as it should be deleted @@ -1488,8 +1488,6 @@ def test_list_delete_token_shows_in_event_list(self): self.head('/auth/tokens', headers=headers, expected_status=404) self.head('/auth/tokens', headers=headers2, expected_status=200) self.head('/auth/tokens', headers=headers3, expected_status=200) - self.head('/auth/tokens', headers=headers_unrevoked, - expected_status=200) def test_list_with_filter(self):
keystone/tests/test_v3_os_revoke.py+1 −2 modified@@ -62,8 +62,7 @@ def test_revoked_token_in_list(self): expires_at = token.default_expire_time() sample = self._blank_event() sample['user_id'] = unicode(user_id) - sample['expires_at'] = unicode(timeutils.isotime(expires_at, - subsecond=True)) + sample['expires_at'] = unicode(timeutils.isotime(expires_at)) before_time = timeutils.utcnow() self.revoke_api.revoke_by_expiration(user_id, expires_at) resp = self.get('/OS-REVOKE/events')
7aee6304f653Fix revocation event handling with MySQL
4 files changed · +31 −18
keystone/contrib/revoke/model.py+22 −6 modified@@ -69,6 +69,11 @@ def __init__(self, **kwargs): # This is revoking all tokens for a domain. self.domain_scope_id = None + if self.expires_at is not None: + # Trim off the expiration time because MySQL timestamps are only + # accurate to the second. + self.expires_at = self.expires_at.replace(microsecond=0) + if self.revoked_at is None: self.revoked_at = timeutils.utcnow() if self.issued_before is None: @@ -90,8 +95,7 @@ def to_dict(self): if self.consumer_id is not None: event['OS-OAUTH1:access_token_id'] = self.access_token_id if self.expires_at is not None: - event['expires_at'] = timeutils.isotime(self.expires_at, - subsecond=True) + event['expires_at'] = timeutils.isotime(self.expires_at) if self.issued_before is not None: event['issued_before'] = timeutils.isotime(self.issued_before, subsecond=True) @@ -242,9 +246,15 @@ def is_revoked(self, token_data): def build_token_values_v2(access, default_domain_id): token_data = access['token'] + + token_expires_at = timeutils.parse_isotime(token_data['expires']) + + # Trim off the microseconds because the revocation event only has + # expirations accurate to the second. + token_expires_at = token_expires_at.replace(microsecond=0) + token_values = { - 'expires_at': timeutils.normalize_time( - timeutils.parse_isotime(token_data['expires'])), + 'expires_at': timeutils.normalize_time(token_expires_at), 'issued_at': timeutils.normalize_time( timeutils.parse_isotime(token_data['issued_at']))} @@ -282,9 +292,15 @@ def build_token_values_v2(access, default_domain_id): def build_token_values(token_data): + + token_expires_at = timeutils.parse_isotime(token_data['expires_at']) + + # Trim off the microseconds because the revocation event only has + # expirations accurate to the second. + token_expires_at = token_expires_at.replace(microsecond=0) + token_values = { - 'expires_at': timeutils.normalize_time( - timeutils.parse_isotime(token_data['expires_at'])), + 'expires_at': timeutils.normalize_time(token_expires_at), 'issued_at': timeutils.normalize_time( timeutils.parse_isotime(token_data['issued_at']))}
keystone/tests/test_revoke.py+3 −3 modified@@ -346,7 +346,7 @@ def test_by_user_expiration(self): event = self._revoke_by_expiration(user_id, future_time) token_data_1 = _sample_blank_token() token_data_1['user_id'] = user_id - token_data_1['expires_at'] = future_time + token_data_1['expires_at'] = future_time.replace(microsecond=0) self._assertTokenRevoked(token_data_1) token_data_2 = _sample_blank_token() @@ -371,7 +371,7 @@ def test_by_user_project(self): token_data = _sample_blank_token() token_data['user_id'] = user_id token_data['project_id'] = project_id - token_data['expires_at'] = future_time + token_data['expires_at'] = future_time.replace(microsecond=0) self._revoke_by_expiration(user_id, future_time, project_id=project_id) self._assertTokenRevoked(token_data) @@ -388,7 +388,7 @@ def test_by_user_domain(self): token_data = _sample_blank_token() token_data['user_id'] = user_id token_data['assignment_domain_id'] = domain_id - token_data['expires_at'] = future_time + token_data['expires_at'] = future_time.replace(microsecond=0) self._revoke_by_expiration(user_id, future_time, domain_id=domain_id) self._assertTokenRevoked(token_data)
keystone/tests/test_v3_auth.py+5 −7 modified@@ -1464,6 +1464,11 @@ def test_disable_domain_shows_in_event_list(self): def assertUserAndExpiryInList(self, events, user_id, expires_at): found = False for e in events: + + # Timestamps in the event list are accurate to second. + expires_at = timeutils.parse_isotime(expires_at) + expires_at = timeutils.isotime(expires_at) + if e['user_id'] == user_id and e['expires_at'] == expires_at: found = True self.assertTrue(found, @@ -1487,14 +1492,9 @@ def test_list_delete_token_shows_in_event_list(self): response.json_body['token'] headers3 = {'X-Subject-Token': response.headers['X-Subject-Token']} - scoped_token = self.get_scoped_token() - headers_unrevoked = {'X-Subject-Token': scoped_token} - self.head('/auth/tokens', headers=headers, expected_status=200) self.head('/auth/tokens', headers=headers2, expected_status=200) self.head('/auth/tokens', headers=headers3, expected_status=200) - self.head('/auth/tokens', headers=headers_unrevoked, - expected_status=200) self.delete('/auth/tokens', headers=headers, expected_status=204) # NOTE(ayoung): not deleting token3, as it should be deleted @@ -1511,8 +1511,6 @@ def test_list_delete_token_shows_in_event_list(self): self.head('/auth/tokens', headers=headers, expected_status=404) self.head('/auth/tokens', headers=headers2, expected_status=200) self.head('/auth/tokens', headers=headers3, expected_status=200) - self.head('/auth/tokens', headers=headers_unrevoked, - expected_status=200) def test_list_with_filter(self):
keystone/tests/test_v3_os_revoke.py+1 −2 modified@@ -64,8 +64,7 @@ def test_revoked_token_in_list(self): expires_at = provider.default_expire_time() sample = self._blank_event() sample['user_id'] = six.text_type(user_id) - sample['expires_at'] = six.text_type(timeutils.isotime(expires_at, - subsecond=True)) + sample['expires_at'] = six.text_type(timeutils.isotime(expires_at)) before_time = timeutils.utcnow() self.revoke_api.revoke_by_expiration(user_id, expires_at) resp = self.get('/OS-REVOKE/events')
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
10- github.com/advisories/GHSA-gmvp-5rf9-mxcmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2014-5251ghsaADVISORY
- rhn.redhat.com/errata/RHSA-2014-1121.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2014-1122.htmlnvdWEB
- www.openwall.com/lists/oss-security/2014/08/15/6nvdWEB
- www.ubuntu.com/usn/USN-2324-1nvdWEB
- bugs.launchpad.net/keystone/+bug/1347961nvdWEB
- github.com/openstack/keystone/commit/6cbf835542d62e6e5db4b4aef7141b1731cad9dcghsaWEB
- github.com/openstack/keystone/commit/7aee6304f653475a4130dc3e5be602e91481f108ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/keystone/PYSEC-2014-107.yamlghsaWEB
News mentions
0No linked articles in our index yet.