Medium severity6.5NVD Advisory· Published Jan 21, 2026· Updated Apr 15, 2026
CVE-2025-14559
CVE-2025-14559
Description
A flaw was found in the keycloak-services component of Keycloak. This vulnerability allows the issuance of access and refresh tokens for disabled users, leading to unauthorized use of previously revoked privileges, via a business logic vulnerability in the Token Exchange implementation when a privileged client invokes the token exchange flow.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-servicesMaven | >= 26.5.0, < 26.5.2 | 26.5.2 |
org.keycloak:keycloak-servicesMaven | < 26.4.9 | 26.4.9 |
Affected products
1Patches
22d0aa31c4830Check if requested user is enabled for impersonation in TE v1 (#45684)
3 files changed · +41 −1
services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/V1TokenExchangeProvider.java+1 −1 modified@@ -138,7 +138,7 @@ protected Response tokenExchange() { requestedUser = session.users().getUserById(realm, requestedSubject); } - if (requestedUser == null) { + if (requestedUser == null || !requestedUser.isEnabled()) { // We always returned access denied to avoid username fishing event.detail(Details.REASON, "requested_subject not found"); event.error(Errors.NOT_ALLOWED);
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/UserAttributeUpdater.java+5 −0 modified@@ -103,6 +103,11 @@ public UserAttributeUpdater setEmailVerified(Boolean emailVerified) { return this; } + public UserAttributeUpdater setEnabled(Boolean enabled) { + rep.setEnabled(enabled); + return this; + } + public UserAttributeUpdater setRequiredActions(UserModel.RequiredAction... requiredAction) { rep.setRequiredActions(Arrays.stream(requiredAction) .map(action -> action.name())
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java+35 −0 modified@@ -44,6 +44,7 @@ import org.keycloak.testsuite.arquillian.annotation.DisableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; +import org.keycloak.testsuite.updaters.UserAttributeUpdater; import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse; import org.keycloak.testsuite.util.oauth.OAuthClient; @@ -182,6 +183,24 @@ public void testImpersonation() throws Exception { assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); } + // disabled user cannot be impersonated + try (UserAttributeUpdater userUpdater = UserAttributeUpdater + .forUserByUsername(adminClient.realm(TEST), "impersonated-user") + .setEnabled(Boolean.FALSE) + .update(); + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + .param(OAuth2Constants.AUDIENCE, "target") + ))) { + Assert.assertEquals(403, response.getStatus()); + } + try (Response response = exchangeUrl.request() .post(Entity.form( new Form() @@ -524,6 +543,22 @@ public void testDirectImpersonation() throws Exception { assertTrue(response.getStatus() >= 400); response.close(); } + + // disabled user cannot be impersonated + try (UserAttributeUpdater userUpdater = UserAttributeUpdater + .forUserByUsername(adminClient.realm(TEST), "impersonated-user") + .setEnabled(Boolean.FALSE) + .update(); + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + .param(OAuth2Constants.AUDIENCE, "target") + ))) { + Assert.assertEquals(403, response.getStatus()); + } }
d67349f3aa9fCheck if requested user is enabled for impersonation in TE v1
3 files changed · +41 −1
services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/V1TokenExchangeProvider.java+1 −1 modified@@ -140,7 +140,7 @@ protected Response tokenExchange() { requestedUser = session.users().getUserById(realm, requestedSubject); } - if (requestedUser == null) { + if (requestedUser == null || !requestedUser.isEnabled()) { // We always returned access denied to avoid username fishing event.detail(Details.REASON, "requested_subject not found"); event.error(Errors.NOT_ALLOWED);
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/UserAttributeUpdater.java+5 −0 modified@@ -106,6 +106,11 @@ public UserAttributeUpdater setEmailVerified(Boolean emailVerified) { return this; } + public UserAttributeUpdater setEnabled(Boolean enabled) { + rep.setEnabled(enabled); + return this; + } + public UserAttributeUpdater setRequiredActions(UserModel.RequiredAction... requiredAction) { rep.setRequiredActions(Arrays.stream(requiredAction) .map(action -> action.name())
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java+35 −0 modified@@ -42,6 +42,7 @@ import org.keycloak.testsuite.arquillian.annotation.DisableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; +import org.keycloak.testsuite.updaters.UserAttributeUpdater; import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse; import org.keycloak.testsuite.util.oauth.OAuthClient; @@ -185,6 +186,24 @@ public void testImpersonation() throws Exception { assertTrue(exchangedToken.getRealmAccess().isUserInRole("example")); } + // disabled user cannot be impersonated + try (UserAttributeUpdater userUpdater = UserAttributeUpdater + .forUserByUsername(adminClient.realm(TEST), "impersonated-user") + .setEnabled(Boolean.FALSE) + .update(); + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + .param(OAuth2Constants.AUDIENCE, "target") + ))) { + Assert.assertEquals(403, response.getStatus()); + } + try (Response response = exchangeUrl.request() .post(Entity.form( new Form() @@ -527,6 +546,22 @@ public void testDirectImpersonation() throws Exception { assertTrue(response.getStatus() >= 400); response.close(); } + + // disabled user cannot be impersonated + try (UserAttributeUpdater userUpdater = UserAttributeUpdater + .forUserByUsername(adminClient.realm(TEST), "impersonated-user") + .setEnabled(Boolean.FALSE) + .update(); + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user") + .param(OAuth2Constants.AUDIENCE, "target") + ))) { + Assert.assertEquals(403, response.getStatus()); + } }
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-wv3h-x6c4-r867ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-14559ghsaADVISORY
- access.redhat.com/errata/RHSA-2026:2365nvdWEB
- access.redhat.com/errata/RHSA-2026:2366nvdWEB
- access.redhat.com/security/cve/CVE-2025-14559nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/keycloak/keycloak/commit/2d0aa31c4830ebaad094c3762e78b884c141e659ghsaWEB
- github.com/keycloak/keycloak/commit/d67349f3aa9fed5c61750619d0f9de6356aeaeffghsaWEB
- github.com/keycloak/keycloak/issues/45651ghsaWEB
- github.com/keycloak/keycloak/releases/tag/26.5.2ghsaWEB
News mentions
0No linked articles in our index yet.