VYPR
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.

PackageAffected versionsPatched versions
org.cloudfoundry.identity:cloudfoundry-identity-serverMaven
>= 77.30.0, < 78.8.078.8.0

Affected products

3

Patches

1
74c88235b5bc

Merge pull request #3743 from cloudfoundry/cve/token-revocation

https://github.com/cloudfoundry/uaaDuane MayFeb 12, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.