VYPR
High severityNVD Advisory· Published Aug 6, 2021· Updated Aug 4, 2024

CVE-2021-38155

CVE-2021-38155

Description

OpenStack Keystone 10.x through 16.x before 16.0.2, 17.x before 17.0.1, 18.x before 18.0.1, and 19.x before 19.0.1 allows information disclosure during account locking (related to PCI DSS features). By guessing the name of an account and failing to authenticate multiple times, any unauthenticated actor could both confirm the account exists and obtain that account's corresponding UUID, which might be leveraged for other unrelated attacks. All deployments enabling security_compliance.lockout_failure_attempts are affected.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

OpenStack Keystone account locking leaks user existence and UUID to unauthenticated attackers via error messages.

Vulnerability

OpenStack Keystone versions 10.x through 16.x before 16.0.2, 17.x before 17.0.1, 18.x before 18.0.1, and 19.x before 19.0.1 contain an information disclosure vulnerability in the account locking feature. When security_compliance.lockout_failure_attempts is enabled, the server returns an AccountLocked exception that includes the user's UUID in the error message. This allows any unauthenticated actor to confirm the existence of an account and obtain its corresponding UUID by repeatedly attempting authentication with a guessed username and an incorrect password. The feature is part of PCI DSS compliance (requirements 8.1.6 and 8.1.7) [1][2][3].

Exploitation

An attacker can exploit this vulnerability by sending multiple authentication requests to the Keystone API using a guessed username and an incorrect password. After the configured number of failed attempts (e.g., 2 attempts when lockout_failure_attempts = 2), the server responds with an AccountLocked error message that contains the user's UUID in the format: "The account is locked for user: ". Before the lockout threshold, the server returns a generic "The request you have made requires authentication." message, which does not reveal whether the account exists. The attacker can also trigger lockout by attempting to change the user's password via /v3/users/<user_id>/password with an incorrect original password, but this requires knowing the user's UUID first. No authentication or prior access is needed for the main attack vector [3].

Impact

Successful exploitation allows an unauthenticated attacker to confirm the existence of user accounts and obtain their UUIDs. This information leakage can be leveraged in further attacks, such as targeted password guessing, social engineering, or other unrelated attacks that rely on knowing valid account identifiers. The vulnerability does not directly allow account takeover or privilege escalation, but it weakens the security of the identity system by exposing internal identifiers [1][2].

Mitigation

The vulnerability is fixed in Keystone versions 16.0.2, 17.0.1, 18.0.1, and 19.0.1. The fix changes the exception handling to return a generic Unauthorized error message instead of AccountLocked, preventing the disclosure of the user UUID. Users should upgrade to the patched versions as soon as possible. The fix is included in commit 8ab4eb27be4c13c9bab2b3ea700f00a190521bf8 [4]. As a workaround, operators can disable the security_compliance.lockout_failure_attempts option, but this may violate PCI DSS requirements for account locking [2][3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
keystonePyPI
>= 10.0, < 16.0.216.0.2
keystonePyPI
>= 17.0, < 17.0.117.0.1
keystonePyPI
>= 18.0, < 18.0.118.0.1
keystonePyPI
>= 19.0, < 19.0.119.0.1

Affected products

82

Patches

3
1b573ae7d1c2

Hide AccountLocked exception from end users

https://github.com/openstack/keystoneGage HugoOct 27, 2020via ghsa
4 files changed · +16 6
  • keystone/notifications.py+2 0 modified
    @@ -580,6 +580,8 @@ def wrapper(wrapped_self, user_id, *args, **kwargs):
                                              taxonomy.OUTCOME_FAILURE,
                                              target, self.event_type,
                                              reason=audit_reason)
    +                if isinstance(ex, exception.AccountLocked):
    +                    raise exception.Unauthorized
                     raise
                 except Exception:
                     # For authentication failure send a CADF event as well
    
  • keystone/tests/unit/common/test_notifications.py+1 1 modified
    @@ -802,7 +802,7 @@ def test_locked_out_user_sends_notification(self):
             password = uuid.uuid4().hex
             new_password = uuid.uuid4().hex
             expected_responses = [AssertionError, AssertionError, AssertionError,
    -                              exception.AccountLocked]
    +                              exception.Unauthorized]
             user_ref = unit.new_user_ref(domain_id=self.domain_id,
                                          password=password)
             user_ref = PROVIDERS.identity_api.create_user(user_ref)
    
  • keystone/tests/unit/identity/test_backend_sql.py+5 5 modified
    @@ -576,7 +576,7 @@ def test_locking_out_user_after_max_failed_attempts(self):
                 )
                 # test locking out user after max failed attempts
                 self._fail_auth_repeatedly(self.user['id'])
    -            self.assertRaises(exception.AccountLocked,
    +            self.assertRaises(exception.Unauthorized,
                                   PROVIDERS.identity_api.authenticate,
                                   user_id=self.user['id'],
                                   password=uuid.uuid4().hex)
    @@ -605,7 +605,7 @@ def test_set_enabled_unlocks_user(self):
             with self.make_request():
                 # lockout user
                 self._fail_auth_repeatedly(self.user['id'])
    -            self.assertRaises(exception.AccountLocked,
    +            self.assertRaises(exception.Unauthorized,
                                   PROVIDERS.identity_api.authenticate,
                                   user_id=self.user['id'],
                                   password=uuid.uuid4().hex)
    @@ -624,7 +624,7 @@ def test_lockout_duration(self):
                 with self.make_request():
                     # lockout user
                     self._fail_auth_repeatedly(self.user['id'])
    -                self.assertRaises(exception.AccountLocked,
    +                self.assertRaises(exception.Unauthorized,
                                       PROVIDERS.identity_api.authenticate,
                                       user_id=self.user['id'],
                                       password=uuid.uuid4().hex)
    @@ -650,7 +650,7 @@ def test_lockout_duration_failed_auth_cnt_resets(self):
                 with self.make_request():
                     # lockout user
                     self._fail_auth_repeatedly(self.user['id'])
    -                self.assertRaises(exception.AccountLocked,
    +                self.assertRaises(exception.Unauthorized,
                                       PROVIDERS.identity_api.authenticate,
                                       user_id=self.user['id'],
                                       password=uuid.uuid4().hex)
    @@ -660,7 +660,7 @@ def test_lockout_duration_failed_auth_cnt_resets(self):
                     # repeat failed auth the max times
                     self._fail_auth_repeatedly(self.user['id'])
                     # test user account is locked
    -                self.assertRaises(exception.AccountLocked,
    +                self.assertRaises(exception.Unauthorized,
                                       PROVIDERS.identity_api.authenticate,
                                       user_id=self.user['id'],
                                       password=uuid.uuid4().hex)
    
  • releasenotes/notes/bug-1688137-e4203c9a728690a7.yaml+8 0 added
    @@ -0,0 +1,8 @@
    +---
    +fixes:
    +  - |
    +    [`bug 1688137 <https://bugs.launchpad.net/keystone/+bug/1688137>`_]
    +    Fixed the AccountLocked exception being shown to the end user since
    +    it provides some information that could be exploited by a
    +    malicious user. The end user will now see Unauthorized instead of
    +    AccountLocked, preventing user info oracle exploitation.
    
ac2631ae3344

Hide AccountLocked exception from end users

https://github.com/openstack/keystoneGage HugoOct 27, 2020via ghsa
4 files changed · +16 6
  • keystone/notifications.py+2 0 modified
    @@ -580,6 +580,8 @@ def wrapper(wrapped_self, user_id, *args, **kwargs):
                                              taxonomy.OUTCOME_FAILURE,
                                              target, self.event_type,
                                              reason=audit_reason)
    +                if isinstance(ex, exception.AccountLocked):
    +                    raise exception.Unauthorized
                     raise
                 except Exception:
                     # For authentication failure send a CADF event as well
    
  • keystone/tests/unit/common/test_notifications.py+1 1 modified
    @@ -802,7 +802,7 @@ def test_locked_out_user_sends_notification(self):
             password = uuid.uuid4().hex
             new_password = uuid.uuid4().hex
             expected_responses = [AssertionError, AssertionError, AssertionError,
    -                              exception.AccountLocked]
    +                              exception.Unauthorized]
             user_ref = unit.new_user_ref(domain_id=self.domain_id,
                                          password=password)
             user_ref = PROVIDERS.identity_api.create_user(user_ref)
    
  • keystone/tests/unit/identity/test_backend_sql.py+5 5 modified
    @@ -613,7 +613,7 @@ def test_locking_out_user_after_max_failed_attempts(self):
                 )
                 # test locking out user after max failed attempts
                 self._fail_auth_repeatedly(self.user['id'])
    -            self.assertRaises(exception.AccountLocked,
    +            self.assertRaises(exception.Unauthorized,
                                   PROVIDERS.identity_api.authenticate,
                                   user_id=self.user['id'],
                                   password=uuid.uuid4().hex)
    @@ -642,7 +642,7 @@ def test_set_enabled_unlocks_user(self):
             with self.make_request():
                 # lockout user
                 self._fail_auth_repeatedly(self.user['id'])
    -            self.assertRaises(exception.AccountLocked,
    +            self.assertRaises(exception.Unauthorized,
                                   PROVIDERS.identity_api.authenticate,
                                   user_id=self.user['id'],
                                   password=uuid.uuid4().hex)
    @@ -661,7 +661,7 @@ def test_lockout_duration(self):
                 with self.make_request():
                     # lockout user
                     self._fail_auth_repeatedly(self.user['id'])
    -                self.assertRaises(exception.AccountLocked,
    +                self.assertRaises(exception.Unauthorized,
                                       PROVIDERS.identity_api.authenticate,
                                       user_id=self.user['id'],
                                       password=uuid.uuid4().hex)
    @@ -687,7 +687,7 @@ def test_lockout_duration_failed_auth_cnt_resets(self):
                 with self.make_request():
                     # lockout user
                     self._fail_auth_repeatedly(self.user['id'])
    -                self.assertRaises(exception.AccountLocked,
    +                self.assertRaises(exception.Unauthorized,
                                       PROVIDERS.identity_api.authenticate,
                                       user_id=self.user['id'],
                                       password=uuid.uuid4().hex)
    @@ -697,7 +697,7 @@ def test_lockout_duration_failed_auth_cnt_resets(self):
                     # repeat failed auth the max times
                     self._fail_auth_repeatedly(self.user['id'])
                     # test user account is locked
    -                self.assertRaises(exception.AccountLocked,
    +                self.assertRaises(exception.Unauthorized,
                                       PROVIDERS.identity_api.authenticate,
                                       user_id=self.user['id'],
                                       password=uuid.uuid4().hex)
    
  • releasenotes/notes/bug-1688137-e4203c9a728690a7.yaml+8 0 added
    @@ -0,0 +1,8 @@
    +---
    +fixes:
    +  - |
    +    [`bug 1688137 <https://bugs.launchpad.net/keystone/+bug/1688137>`_]
    +    Fixed the AccountLocked exception being shown to the end user since
    +    it provides some information that could be exploited by a
    +    malicious user. The end user will now see Unauthorized instead of
    +    AccountLocked, preventing user info oracle exploitation.
    
8ab4eb27be4c

Hide AccountLocked exception from end users

https://github.com/openstack/keystoneGage HugoOct 27, 2020via ghsa
4 files changed · +16 6
  • keystone/notifications.py+2 0 modified
    @@ -580,6 +580,8 @@ def wrapper(wrapped_self, user_id, *args, **kwargs):
                                              taxonomy.OUTCOME_FAILURE,
                                              target, self.event_type,
                                              reason=audit_reason)
    +                if isinstance(ex, exception.AccountLocked):
    +                    raise exception.Unauthorized
                     raise
                 except Exception:
                     # For authentication failure send a CADF event as well
    
  • keystone/tests/unit/common/test_notifications.py+1 1 modified
    @@ -802,7 +802,7 @@ def test_locked_out_user_sends_notification(self):
             password = uuid.uuid4().hex
             new_password = uuid.uuid4().hex
             expected_responses = [AssertionError, AssertionError, AssertionError,
    -                              exception.AccountLocked]
    +                              exception.Unauthorized]
             user_ref = unit.new_user_ref(domain_id=self.domain_id,
                                          password=password)
             user_ref = PROVIDERS.identity_api.create_user(user_ref)
    
  • keystone/tests/unit/identity/test_backend_sql.py+5 5 modified
    @@ -576,7 +576,7 @@ def test_locking_out_user_after_max_failed_attempts(self):
                 )
                 # test locking out user after max failed attempts
                 self._fail_auth_repeatedly(self.user['id'])
    -            self.assertRaises(exception.AccountLocked,
    +            self.assertRaises(exception.Unauthorized,
                                   PROVIDERS.identity_api.authenticate,
                                   user_id=self.user['id'],
                                   password=uuid.uuid4().hex)
    @@ -605,7 +605,7 @@ def test_set_enabled_unlocks_user(self):
             with self.make_request():
                 # lockout user
                 self._fail_auth_repeatedly(self.user['id'])
    -            self.assertRaises(exception.AccountLocked,
    +            self.assertRaises(exception.Unauthorized,
                                   PROVIDERS.identity_api.authenticate,
                                   user_id=self.user['id'],
                                   password=uuid.uuid4().hex)
    @@ -624,7 +624,7 @@ def test_lockout_duration(self):
                 with self.make_request():
                     # lockout user
                     self._fail_auth_repeatedly(self.user['id'])
    -                self.assertRaises(exception.AccountLocked,
    +                self.assertRaises(exception.Unauthorized,
                                       PROVIDERS.identity_api.authenticate,
                                       user_id=self.user['id'],
                                       password=uuid.uuid4().hex)
    @@ -650,7 +650,7 @@ def test_lockout_duration_failed_auth_cnt_resets(self):
                 with self.make_request():
                     # lockout user
                     self._fail_auth_repeatedly(self.user['id'])
    -                self.assertRaises(exception.AccountLocked,
    +                self.assertRaises(exception.Unauthorized,
                                       PROVIDERS.identity_api.authenticate,
                                       user_id=self.user['id'],
                                       password=uuid.uuid4().hex)
    @@ -660,7 +660,7 @@ def test_lockout_duration_failed_auth_cnt_resets(self):
                     # repeat failed auth the max times
                     self._fail_auth_repeatedly(self.user['id'])
                     # test user account is locked
    -                self.assertRaises(exception.AccountLocked,
    +                self.assertRaises(exception.Unauthorized,
                                       PROVIDERS.identity_api.authenticate,
                                       user_id=self.user['id'],
                                       password=uuid.uuid4().hex)
    
  • releasenotes/notes/bug-1688137-e4203c9a728690a7.yaml+8 0 added
    @@ -0,0 +1,8 @@
    +---
    +fixes:
    +  - |
    +    [`bug 1688137 <https://bugs.launchpad.net/keystone/+bug/1688137>`_]
    +    Fixed the AccountLocked exception being shown to the end user since
    +    it provides some information that could be exploited by a
    +    malicious user. The end user will now see Unauthorized instead of
    +    AccountLocked, preventing user info oracle exploitation.
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.