CVE-2026-3429
Description
A flaw was identified in the Account REST API of Keycloak that allows a user authenticated at a lower security level to perform sensitive actions intended only for higher-assurance sessions. Specifically, an attacker who has already obtained a victim’s password can delete the victim’s registered MFA/OTP credential without first proving possession of that factor. The attacker can then register their own MFA device, effectively taking full control of the account. This weakness undermines the intended protection provided by multi-factor authentication.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-servicesMaven | <= 26.5.6 | — |
Patches
168f5779230d0CVE-2026-3429 Improper Access Control for LoA During Credential Deletion for the case of client overriden flow
3 files changed · +64 −16
server-spi-private/src/main/java/org/keycloak/models/utils/AuthenticationFlowResolver.java+16 −13 modified@@ -49,14 +49,9 @@ public static AuthenticationFlowModel resolveBrowserFlow(AuthenticationSessionMo } } - String clientFlow = client.getAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING); - if (clientFlow != null) { - flow = authSession.getRealm().getAuthenticationFlowById(clientFlow); - if (flow != null) { - return flow; - } - logger.warnf("Client %s has browser flow override, but this flow '%s' does not exist, " + - "fallback to browser flow", client.getClientId(), clientFlow); + flow = resolveBindingOverrideFlowForClient(client, AuthenticationFlowBindings.BROWSER_BINDING); + if (flow != null) { + return flow; } return authSession.getRealm().getBrowserFlow(); } @@ -76,15 +71,23 @@ public static AuthenticationFlowModel resolveDirectGrantFlow(AuthenticationSessi } } - String clientFlow = client.getAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING); + flow = resolveBindingOverrideFlowForClient(client, AuthenticationFlowBindings.DIRECT_GRANT_BINDING); + if (flow != null) { + return flow; + } + return authSession.getRealm().getDirectGrantFlow(); + } + + public static AuthenticationFlowModel resolveBindingOverrideFlowForClient(ClientModel client, String flowBindingType) { + String clientFlow = client.getAuthenticationFlowBindingOverride(flowBindingType); if (clientFlow != null) { - flow = authSession.getRealm().getAuthenticationFlowById(clientFlow); + AuthenticationFlowModel flow = client.getRealm().getAuthenticationFlowById(clientFlow); if (flow != null) { return flow; } - logger.warnf("Client %s has direct grant flow override, but this flow '%s' does not exist, " + - "fallback to direct grant flow", client.getClientId(), clientFlow); + logger.warnf("Client %s has %s flow override, but configured override flow '%s' does not exist, " + + "fallback to realm %s flow", client.getClientId(), flowBindingType, clientFlow, flowBindingType); } - return authSession.getRealm().getDirectGrantFlow(); + return null; } }
services/src/main/java/org/keycloak/authentication/requiredactions/util/CredentialDeleteHelper.java+14 −3 modified@@ -32,9 +32,13 @@ import org.keycloak.credential.CredentialProvider; import org.keycloak.credential.CredentialTypeMetadata; import org.keycloak.credential.CredentialTypeMetadataContext; +import org.keycloak.models.AuthenticationFlowBindings; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.AuthenticationFlowResolver; import org.jboss.logging.Logger; @@ -107,16 +111,23 @@ private static void checkIfCanBeRemoved(KeycloakSession session, UserModel user, } private static void checkAuthenticatedLoASufficientForCredentialRemove(KeycloakSession session, String credentialType, Supplier<Integer> currentLoAProvider) { - int requestedLoaForCredentialRemove = getRequestedLoaForCredential(session, session.getContext().getRealm(), credentialType); + int requestedLoaForCredentialRemove = getRequestedLoaForCredential(session, credentialType); int currentAuthenticatedLevel = currentLoAProvider.get(); if (currentAuthenticatedLevel < requestedLoaForCredentialRemove) { throw new ForbiddenException("Insufficient level of authentication for removing credential of type '" + credentialType + "'."); } } - private static int getRequestedLoaForCredential(KeycloakSession session, RealmModel realm, String credentialType) { - Map<String, Integer> credentialTypesToLoa = LoAUtil.getCredentialTypesToLoAMap(session, realm, realm.getBrowserFlow()); + private static int getRequestedLoaForCredential(KeycloakSession session, String credentialType) { + RealmModel realm = session.getContext().getRealm(); + ClientModel client = session.getContext().getClient(); + AuthenticationFlowModel authFlow = AuthenticationFlowResolver.resolveBindingOverrideFlowForClient(client, AuthenticationFlowBindings.BROWSER_BINDING); + if (authFlow == null) { + authFlow = realm.getBrowserFlow(); + } + + Map<String, Integer> credentialTypesToLoa = LoAUtil.getCredentialTypesToLoAMap(session, realm, authFlow); return credentialTypesToLoa.getOrDefault(credentialType, NO_LOA); } }
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LevelOfAssuranceFlowTest.java+34 −0 modified@@ -55,11 +55,13 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; import org.keycloak.representations.ClaimsRepresentation; import org.keycloak.representations.IDToken; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AbstractAuthenticationTest; import org.keycloak.testsuite.AbstractChangeImportedUserPasswordsTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; @@ -850,6 +852,38 @@ public void testWithMultipleOTPCodes() throws Exception { } } + // Test that LoA enforcement works for the case when there is "AuthenticationFlowBindingOverride" with the enforced LoA flow for the particular client + @Test + public void testWithMultipleOTPCodes_clientSpecificAuthenticationFlow() throws Exception { + List<AuthenticationFlowRepresentation> flows = testRealm().flows().getFlows(); + + // Set default browser flow for realm + RealmRepresentation realm = testRealm().toRepresentation(); + realm.setBrowserFlow(DefaultAuthenticationFlows.BROWSER_FLOW); + testRealm().update(realm); + + // Override step-up flow just for the client + AuthenticationFlowRepresentation stepUpFlowRepresentation = AbstractAuthenticationTest.findFlowByAlias(FLOW_ALIAS, flows); + ClientResource testClient = ApiUtil.findClientByClientId(testRealm(), CLIENT_ID); + ClientRepresentation testClientRep = testClient.toRepresentation(); + testClientRep.setAuthenticationFlowBindingOverrides( + Map.of(DefaultAuthenticationFlows.BROWSER_FLOW, stepUpFlowRepresentation.getId()) + ); + testClient.update(testClientRep); + + try { + testWithMultipleOTPCodes(); + } finally { + // Revert + testClientRep.setAuthenticationFlowBindingOverrides( + Map.of(DefaultAuthenticationFlows.BROWSER_FLOW, "") + ); + testClient.update(testClientRep); + realm.setBrowserFlow(FLOW_ALIAS); + testRealm().update(realm); + } + } + @Test public void testDeleteCredentialAction() throws Exception { // Login level1
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
8- github.com/advisories/GHSA-8g9r-9wjw-37j4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-3429ghsaADVISORY
- access.redhat.com/errata/RHSA-2026:6477nvdWEB
- access.redhat.com/errata/RHSA-2026:6478nvdWEB
- access.redhat.com/security/cve/CVE-2026-3429nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/keycloak/keycloak/commit/68f5779230d08825e6a4b4e23471fade16434178ghsaWEB
- github.com/keycloak/keycloak/issues/47069ghsaWEB
News mentions
0No linked articles in our index yet.