VYPR
Medium severity4.2NVD Advisory· Published Mar 11, 2026· Updated Apr 2, 2026

CVE-2026-3429

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.

PackageAffected versionsPatched versions
org.keycloak:keycloak-servicesMaven
<= 26.5.6

Patches

1
68f5779230d0

CVE-2026-3429 Improper Access Control for LoA During Credential Deletion for the case of client overriden flow

https://github.com/keycloak/keycloakmposoldaMar 24, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.