VYPR
Moderate severityNVD Advisory· Published May 30, 2025· Updated May 30, 2025

Bypass of System Admin User Deactivation Controls for Personal Access Tokens in Mattermost Server

CVE-2025-3230

Description

Mattermost versions 10.7.x <= 10.7.0, 10.6.x <= 10.6.2, 10.5.x <= 10.5.3, 9.11.x <= 9.11.12 fails to properly invalidate personal access tokens upon user deactivation, allowing deactivated users to maintain full system access by exploiting access token validation flaws via continued usage of previously issued tokens.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost/server/v8Go
>= 10.7.0-rc1, < 10.7.110.7.1
github.com/mattermost/mattermost/server/v8Go
>= 10.6.0-rc1, < 10.6.310.6.3
github.com/mattermost/mattermost/server/v8Go
>= 10.0.0-rc1, < 10.5.410.5.4
github.com/mattermost/mattermost/server/v8Go
>= 9.0.0-rc1, < 9.11.139.11.13
github.com/mattermost/mattermost/server/v8Go
< 8.0.0-20250402193107-65343f84a7838.0.0-20250402193107-65343f84a783

Affected products

1

Patches

1
65343f84a783

[MM-63480] Remove user cache early when deactivating user (#30571)

https://github.com/mattermost/mattermostJulien TantApr 2, 2025via ghsa
2 files changed · +126 2
  • e2e-tests/cypress/tests/integration/channels/users/deactivated_user_spec.ts+125 0 added
    @@ -0,0 +1,125 @@
    +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
    +// See LICENSE.txt for license information.
    +
    +// ***************************************************************
    +// - [#] indicates a test step (e.g. # Go to a page)
    +// - [*] indicates an assertion (e.g. * Check the title)
    +// - Use element ID when selecting an element. Create one if none.
    +// ***************************************************************
    +
    +// Stage: @prod
    +// Group: @channels @system_console @user_management
    +
    +import {Team} from '@mattermost/types/teams';
    +import {UserProfile} from '@mattermost/types/users';
    +
    +describe('Deactivated user', () => {
    +    let testUser: UserProfile;
    +    let adminUser: UserProfile;
    +    let testTeam: Team;
    +    let personalAccessToken: string;
    +
    +    before(() => {
    +        // # Set up admin user and team
    +        cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true}).then(({user, team}) => {
    +            adminUser = user;
    +            testTeam = team;
    +
    +            // # Enable Personal Access Token And User Deactivation
    +            cy.apiUpdateConfig({
    +                ServiceSettings: {
    +                    EnableUserAccessTokens: true,
    +                },
    +                TeamSettings: {
    +                    EnableUserDeactivation: true,
    +                },
    +            });
    +        });
    +    });
    +
    +    it('should not allow API access with PAT after user deactivation', () => {
    +        // # Login as admin
    +        cy.apiLogin(adminUser);
    +
    +        // # Create a new user
    +        cy.apiCreateUser().then(({user}) => {
    +            testUser = user;
    +
    +            // # Add user to team
    +            cy.apiAddUserToTeam(testTeam.id, user.id);
    +
    +            // # Grant user permission to create personal access tokens
    +            cy.apiPatchUserRoles(testUser.id, ['system_user', 'system_user_access_token']);
    +
    +            // # Logout admin
    +            cy.apiLogout();
    +
    +            // # Login as the test user
    +            cy.apiLogin(testUser);
    +
    +            // # Navigate to the home page
    +            cy.visit('/');
    +
    +            // # Create a personal access token as the test user
    +            const tokenName = 'token' + Date.now();
    +
    +            // # Generate a personal access token via API
    +            cy.apiAccessToken(testUser.id, tokenName).then((token) => {
    +                personalAccessToken = token.token;
    +
    +                // # Replace the auth cookie with the PAT
    +                cy.setCookie('MMAUTHTOKEN', personalAccessToken);
    +
    +                // # Reload the page to use the PAT for authentication
    +                cy.visit('/');
    +
    +                // * Verify the auth cookie has been set with the PAT
    +                cy.getCookie('MMAUTHTOKEN').
    +                    should('have.property', 'value', personalAccessToken);
    +
    +                // * Verify we're still logged in using the PAT
    +                cy.get('#sidebarItem_town-square').should('be.visible');
    +
    +                // # Make an API request using the PAT
    +                cy.request({
    +                    headers: {
    +                        Authorization: `Bearer ${personalAccessToken}`,
    +                    },
    +                    url: '/api/v4/users/me',
    +                    method: 'GET',
    +                }).then((response) => {
    +                    // * Verify the request was successful
    +                    expect(response.status).to.equal(200);
    +
    +                    // * Verify the response contains the correct user ID
    +                    expect(response.body.id).to.equal(testUser.id);
    +                });
    +
    +                // # Use an admin client to deactivate the user
    +                cy.makeClient({user: adminUser}).then((client) => {
    +                    // # Deactivate the test user
    +                    client.updateUserActive(testUser.id, false).then(() => {
    +                        // # Try to use the PAT after user deactivation
    +                        cy.request({
    +                            headers: {
    +                                Authorization: `Bearer ${personalAccessToken}`,
    +                            },
    +                            url: '/api/v4/users/me',
    +                            method: 'GET',
    +                            failOnStatusCode: false,
    +                        }).then((response) => {
    +                            // * Verify the request fails with 401 Unauthorized
    +                            expect(response.status).to.equal(401);
    +                        });
    +
    +                        // # Try to navigate back to the channel
    +                        cy.visit(`/${testTeam.name}/channels/town-square`);
    +
    +                        // * Verify we are redirect to the login page
    +                        cy.url().should('include', '/login');
    +                    });
    +                });
    +            });
    +        });
    +    });
    +});
    
  • server/channels/app/user.go+1 2 modified
    @@ -1044,6 +1044,7 @@ func (a *App) UpdateActive(c request.CTX, user *model.User, active bool) (*model
     		}
     	}
     	ruser := userUpdate.New
    +	a.InvalidateCacheForUser(user.Id)
     
     	if !active {
     		if err := a.RevokeAllSessions(c, ruser.Id); err != nil {
    @@ -1057,8 +1058,6 @@ func (a *App) UpdateActive(c request.CTX, user *model.User, active bool) (*model
     	if appErr := a.invalidateUserChannelMembersCaches(c, user.Id); appErr != nil {
     		c.Logger().Warn("Error while invalidating user channel members caches", mlog.Err(appErr))
     	}
    -	a.InvalidateCacheForUser(user.Id)
    -
     	a.sendUpdatedUserEvent(ruser)
     
     	if !active && user.DeleteAt != 0 {
    

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

4

News mentions

0

No linked articles in our index yet.