Medium severity6.5NVD Advisory· Published Mar 5, 2026· Updated May 10, 2026
CVE-2026-22723
CVE-2026-22723
Description
Inappropriate user token revocation due to a logic error in the token revocation endpoint implementation in Cloudfoundry UAA v77.30.0 to v78.7.0 and in Cloudfoundry Deployment v48.7.0 to v54.10.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven | >= 77.30.0, < 78.8.0 | 78.8.0 |
Affected products
3- cpe:2.3:a:cloudfoundry:cf-deployment:*:*:*:*:*:*:*:*Range: >48.7.0,<=54.11.0
- Range: 77.30.0
Patches
174c88235b5bcMerge pull request #3743 from cloudfoundry/cve/token-revocation
3 files changed · +109 −14
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/beans/OauthEndpointSecurityConfiguration.java+1 −2 modified@@ -239,8 +239,7 @@ UaaFilterChain tokenRevocationFilter(HttpSecurity http, @Qualifier("self") IsSel .authorizeHttpRequests( auth -> { auth.requestMatchers("/oauth/token/revoke/client/**").access(anyOf(true).hasScope("tokens.revoke").isUaaAdmin().isZoneAdmin()); auth.requestMatchers("/oauth/token/revoke/user/*/client/**").access(anyOf(true).hasScope("tokens.revoke").isUaaAdmin().isZoneAdmin() - .or(SelfCheckAuthorizationManager.isUserTokenRevocationForSelf(selfCheck, 4)) - .or(SelfCheckAuthorizationManager.isClientTokenRevocationForSelf(selfCheck, 6)) + .or(SelfCheckAuthorizationManager.isClientUserTokenRevocationForSelf(selfCheck, 6, 4)) ); auth.requestMatchers("/oauth/token/revoke/user/**").access(anyOf(true) .hasScope("tokens.revoke").isUaaAdmin()
server/src/main/java/org/cloudfoundry/identity/uaa/web/SelfCheckAuthorizationManager.java+32 −1 modified@@ -10,9 +10,16 @@ import org.springframework.security.web.access.intercept.RequestAuthorizationContext; public class SelfCheckAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> { - private enum CheckType {USER, TOKEN_REVOCATION_USER, TOKEN_REVOCATION_CLIENT, TOKEN_REVOCATION_SELF}; + private enum CheckType { + USER, + TOKEN_REVOCATION_USER, + TOKEN_REVOCATION_CLIENT, + TOKEN_REVOCATION_SELF, + TOKEN_REVOCATION_CLIENT_USER + }; private final IsSelfCheck selfCheck; private final int parameterIndex; + private final int parameterIndex2; private final CheckType type; @@ -28,14 +35,31 @@ public static SelfCheckAuthorizationManager isClientTokenRevocationForSelf(IsSel return new SelfCheckAuthorizationManager(CheckType.TOKEN_REVOCATION_CLIENT, selfCheck, parameterIndex); } + public static SelfCheckAuthorizationManager isClientUserTokenRevocationForSelf( + IsSelfCheck selfCheck, + int clientParameterIndex, + int userParameterIndex + ) { + return new SelfCheckAuthorizationManager( + CheckType.TOKEN_REVOCATION_CLIENT_USER, + selfCheck, + clientParameterIndex, + userParameterIndex + ); + } + public static SelfCheckAuthorizationManager isTokenRevocationForSelf(IsSelfCheck selfCheck, int parameterIndex) { return new SelfCheckAuthorizationManager(CheckType.TOKEN_REVOCATION_SELF, selfCheck, parameterIndex); } private SelfCheckAuthorizationManager(CheckType type, IsSelfCheck selfCheck, int parameterIndex) { + this(type, selfCheck, parameterIndex, -1); + } + private SelfCheckAuthorizationManager(CheckType type, IsSelfCheck selfCheck, int parameterIndex, int parameterIndex2) { this.type = type; this.selfCheck = selfCheck; this.parameterIndex = parameterIndex; + this.parameterIndex2 = parameterIndex2; } @Override @@ -62,6 +86,13 @@ public AuthorizationDecision check(Supplier<Authentication> authentication, Requ return new AuthorizationDecision(true); } } + case TOKEN_REVOCATION_CLIENT_USER -> { + if (this.selfCheck.isClientTokenRevocationForSelf(request, this.parameterIndex) && + this.selfCheck.isUserTokenRevocationForSelf(request, this.parameterIndex2) + ) { + return new AuthorizationDecision(true); + } + } case TOKEN_REVOCATION_SELF -> { if (this.selfCheck.isTokenRevocationForSelf(request, this.parameterIndex)) { return new AuthorizationDecision(true);
uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenRevocationEndpointMockMvcTest.java+76 −11 modified@@ -81,12 +81,14 @@ void revokeOwnJWToken() throws Exception { TokenRevocationEvent tokenRevocationEvent = tokenRevocationEventListener.getEvents().getFirst(); assertThat(tokenRevocationEvent.getClientId()).isEqualTo(client.getClientId()); assertThat(tokenRevocationEvent.getUserId()).isNull(); - assertThat(tokenRevocationEvent.getAuditEvent().getData()).contains(client.getClientId()); - assertThat(tokenRevocationEvent.getAuditEvent().getData()).doesNotContain("UserID"); + assertThat(tokenRevocationEvent.getAuditEvent().getData()) + .contains(client.getClientId()) + .doesNotContain("UserID"); assertThat(tokenRevocationEvent.getAuditEvent().getOrigin()).contains(client.getClientId()); revocableTokenProvisioning.retrieve(jti, IdentityZoneHolder.get().getId()); fail("Expected EmptyResultDataAccessException to be thrown for revoked token"); } catch (EmptyResultDataAccessException ignored) { + // expected } finally { defaultZone.getConfig().getTokenPolicy().setJwtRevocable(false); identityZoneProvisioning.update(defaultZone); @@ -202,7 +204,7 @@ void revokeOtherClientTokenByClientId(String scope) throws Exception { revocableTokenProvisioning.retrieve(tokenToBeRevoked, IdentityZoneHolder.get().getId()); fail("Token should have been deleted"); } catch (EmptyResultDataAccessException e) { - //expected + // expected } } @@ -323,8 +325,10 @@ void revoke_all_client_tokens() throws Exception { ).andExpect(status().isOk()); assertThat(tokenRevocationEventListener.getEventCount()).isOne(); assertThat(tokenRevocationEventListener.getEvents().getFirst().getClientId()).isEqualTo(client.getClientId()); - assertThat(tokenRevocationEventListener.getEvents().getFirst().getUserId()).as("Event for client based revocation should not contain userid").isNull(); - assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()).contains(client.getClientId()) + assertThat(tokenRevocationEventListener.getEvents().getFirst().getUserId()) + .as("Event for client based revocation should not contain userid").isNull(); + assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()) + .contains(client.getClientId()) .doesNotContain("UserID"); assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getOrigin()).contains("admin"); @@ -376,8 +380,9 @@ void revoke_all_tokens_for_user() throws Exception { assertThat(tokenRevocationEventListener.getEventCount()).isOne(); assertThat(tokenRevocationEventListener.getEvents().getFirst().getUserId()).isEqualTo(user.getId()); assertThat(tokenRevocationEventListener.getEvents().getFirst().getClientId()).isNull(); - assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()).contains(user.getId()); - assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()).doesNotContain("ClientID"); + assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()) + .contains(user.getId()) + .doesNotContain("ClientID"); assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getOrigin()).contains("admin"); //should fail with 401 mockMvc.perform( @@ -416,8 +421,9 @@ void aUserCanRevokeTheirOwnToken() throws Exception { assertThat(tokenRevocationEventListener.getEventCount()).isOne(); assertThat(tokenRevocationEventListener.getEvents().getFirst().getUserId()).isEqualTo(user.getId()); assertThat(tokenRevocationEventListener.getEvents().getFirst().getClientId()).isNull(); - assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()).contains(user.getId()); - assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()).doesNotContain("ClientID"); + assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()) + .contains(user.getId()) + .doesNotContain("ClientID"); assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getOrigin()).contains(user.getUserName()); //should fail with 401 @@ -428,6 +434,64 @@ void aUserCanRevokeTheirOwnToken() throws Exception { .andExpect(content().string(containsString("\"error\":\"invalid_token\""))); } + @Test + void aUserCannotRevokeAnotherUsersClientTokens() throws Exception { + IdentityZone zone = IdentityZoneHolder.get(); + UaaClientDetails client = getAClientWithClientsRead(); + ScimUser user2 = setUpUser(generator.generate().toLowerCase() + "@test.org"); + user2.setPassword("secret"); + + String userInfoToken2 = getUserOAuthAccessToken( + mockMvc, + client.getClientId(), + client.getClientSecret(), + user2.getUserName(), + user2.getPassword(), + "openid", + zone, + true + ); + + //ensure user2's token works + mockMvc.perform( + get("/userinfo").header("Authorization", "Bearer " + userInfoToken2) + ).andExpect(status().isOk()); + + ScimUser user1 = setUpUser(generator.generate().toLowerCase() + "@test.org"); + user1.setPassword("secret"); + + String userInfoToken1 = getUserOAuthAccessToken( + mockMvc, + client.getClientId(), + client.getClientSecret(), + user1.getUserName(), + user1.getPassword(), + "openid", + zone, + true + ); + + //ensure user1's token works + mockMvc.perform( + get("/userinfo").header("Authorization", "Bearer " + userInfoToken1) + ).andExpect(status().isOk()); + + // attempt to revoke user1's tokens while authenticated as user2 + mockMvc.perform( + get("/oauth/token/revoke/user/" + user1.getId() + "/client/" + client.getClientId()) + .header("Authorization", "Bearer " + userInfoToken2) + ) + .andExpect(status().isForbidden()); + + //ensure both tokens work + mockMvc.perform( + get("/userinfo").header("Authorization", "Bearer " + userInfoToken1) + ).andExpect(status().isOk()); + mockMvc.perform( + get("/userinfo").header("Authorization", "Bearer " + userInfoToken2) + ).andExpect(status().isOk()); + } + private void revokeUserClientCombinationTokenWithAuth() throws Exception { UaaClientDetails client = getAClientWithClientsRead(); UaaClientDetails otherClient = getAClientWithClientsRead(); @@ -498,8 +562,9 @@ private void revokeUserClientCombinationTokenWithAuth() throws Exception { assertThat(tokenRevocationEventListener.getEventCount()).isOne(); assertThat(tokenRevocationEventListener.getEvents().getFirst().getClientId()).isEqualTo(client.getClientId()); assertThat(tokenRevocationEventListener.getEvents().getFirst().getUserId()).isEqualTo(user1.getId()); - assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()).contains(client.getClientId()); - assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()).contains(user1.getId()); + assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getData()) + .contains(client.getClientId()) + .contains(user1.getId()); assertThat(tokenRevocationEventListener.getEvents().getFirst().getAuditEvent().getOrigin()).contains("admin"); //should fail with 401
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
6- github.com/advisories/GHSA-6wcw-r64p-qrrwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22723ghsaADVISORY
- www.cloudfoundry.org/blog/cve-2026-22723-uaa-user-token-revocation/nvdMitigationVendor Advisory
- github.com/cloudfoundry/uaa/commit/74c88235b5bc6e61752624700e91f61fd724dfcdghsaWEB
- github.com/cloudfoundry/uaa/releases/tag/v78.8.0ghsaWEB
- www.cloudfoundry.org/blog/cve-2026-22723-uaa-user-token-revocationghsaWEB
News mentions
0No linked articles in our index yet.