Low severityNVD Advisory· Published Mar 16, 2026· Updated Mar 16, 2026
Password Change Bypass via Auth Switch Endpoint
CVE-2026-22545
Description
Mattermost versions 10.11.x <= 10.11.10 fail to validate user's authentication method when processing account auth type switch which allows an authenticated attacker to change account password without confirmation via falsely claiming a different auth provider.. Mattermost Advisory ID: MMSA-2026-00583
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/mattermost/mattermost/server/v8Go | < 8.0.0-20260127144908-ced9a56e3988 | 8.0.0-20260127144908-ced9a56e3988 |
github.com/mattermost/mattermost-serverGo | < 5.3.2-0.20260127144908-ced9a56e3988 | 5.3.2-0.20260127144908-ced9a56e3988 |
github.com/mattermost/mattermost-serverGo | >= 10.11.0-rc1, < 10.11.11 | 10.11.11 |
github.com/mattermost/mattermost-serverGo | >= 11.2.0-rc1, < 11.2.3 | 11.2.3 |
github.com/mattermost/mattermost-serverGo | >= 11.3.0-rc1, < 11.3.1 | 11.3.1 |
Affected products
1- Range: 10.11.0
Patches
1ced9a56e3988[MM-67126] Deprecate UpdateAccessControlPolicyActiveStatus API in favor of new one (#34940)
12 files changed · +98 −45
api/v4/source/access_control.yaml+5 −0 modified@@ -283,11 +283,16 @@ $ref: "#/components/responses/InternalServerError" "/api/v4/access_control_policies/{policy_id}/activate": get: + deprecated: true tags: - access control summary: Activate or deactivate an access control policy description: | Updates the active status of an access control policy. + + **Deprecated:** This endpoint will be removed in a future release. Use the dedicated access control policy update endpoint instead. + Link: </api/v4/access_control_policies/activate>; rel="successor-version" + ##### Permissions Must have the `manage_system` permission. operationId: UpdateAccessControlPolicyActiveStatus
server/channels/api4/access_control.go+22 −4 modified@@ -382,12 +382,22 @@ func searchAccessControlPolicies(c *Context, w http.ResponseWriter, r *http.Requ } } +// updateActiveStatus updates the active status of a single access control policy. +// +// Deprecated: This endpoint is deprecated and will be removed in a future release. +// Use PUT /api/v4/access_control/policies/activate instead, which supports batch updates. func updateActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePolicyId() if c.Err != nil { return } + // CSRF barrier: only allow header-based auth (reject cookie-only sessions) + if r.Header.Get(model.HeaderAuth) == "" { + c.SetInvalidParam("Authorization") + return + } + policyID := c.Params.PolicyId // Check if user has system admin permission OR channel-specific permission for this policy @@ -416,7 +426,11 @@ func updateActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) { } model.AddEventParameterToAuditRec(auditRec, "active", activeBool) - appErr := c.App.UpdateAccessControlPolicyActive(c.AppContext, policyID, activeBool) + // Wrap single update in slice to use the batch update method + updates := []model.AccessControlPolicyActiveUpdate{ + {ID: policyID, Active: activeBool}, + } + _, appErr := c.App.UpdateAccessControlPoliciesActive(c.AppContext, updates) if appErr != nil { c.Err = appErr return @@ -429,6 +443,9 @@ func updateActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) { "status": "OK", } + // Set deprecation header to inform clients + w.Header().Set("Deprecation", "true") + w.Header().Set("Link", "</api/v4/access_control/policies/activate>; rel=\"successor-version\"") w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(response); err != nil { c.Logger.Warn("Error while writing response", mlog.Err(err)) @@ -446,12 +463,13 @@ func setActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) { defer c.LogAuditRec(auditRec) model.AddEventParameterAuditableToAuditRec(auditRec, "requested", &list) - // Check if user has system admin permission OR channel-specific permission for this policy + // Check if user has system admin permission OR policy-specific permission hasManageSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) if !hasManageSystemPermission { for _, entry := range list.Entries { - hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, entry.ID, model.PermissionManageChannelAccessRules) - if !hasChannelPermission { + // Validate policy access permission - this fetches the policy first to verify it exists + // and is a channel-type policy before checking channel permissions + if appErr := c.App.ValidateAccessControlPolicyPermission(c.AppContext, c.AppContext.Session().UserId, entry.ID); appErr != nil { c.SetPermissionError(model.PermissionManageChannelAccessRules) return }
server/channels/api4/access_control_local.go+0 −1 modified@@ -21,7 +21,6 @@ func (api *API) InitAccessControlPolicyLocal() { api.BaseRoutes.AccessControlPolicy.Handle("", api.APILocal(getAccessControlPolicy)).Methods(http.MethodGet) api.BaseRoutes.AccessControlPolicy.Handle("", api.APILocal(deleteAccessControlPolicy)).Methods(http.MethodDelete) - api.BaseRoutes.AccessControlPolicy.Handle("/activate", api.APILocal(updateActiveStatus)).Methods(http.MethodGet) api.BaseRoutes.AccessControlPolicy.Handle("/assign", api.APILocal(assignAccessPolicy)).Methods(http.MethodPost) api.BaseRoutes.AccessControlPolicy.Handle("/unassign", api.APILocal(unassignAccessPolicy)).Methods(http.MethodDelete) api.BaseRoutes.AccessControlPolicy.Handle("/resources/channels", api.APILocal(getChannelsForAccessControlPolicy)).Methods(http.MethodGet)
server/channels/api4/access_control_test.go+62 −0 modified@@ -963,6 +963,7 @@ func TestSetActiveStatus(t *testing.T) { } mockAccessControlService := &mocks.AccessControlServiceInterface{} + mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), privateChannel.Id).Return(channelPolicy, nil) th.App.Srv().Channels().AccessControl = mockAccessControlService // Channel admin should be able to set active status for their channel @@ -974,4 +975,65 @@ func TestSetActiveStatus(t *testing.T) { require.Equal(t, channelPolicy.ID, policies[0].ID, "expected policy ID to match") require.True(t, policies[0].Active, "expected policy to be active") }) + + t.Run("SetActiveStatus with channel admin for another channel should fail", func(t *testing.T) { + // This test verifies the security fix: a channel admin cannot modify the active status + // of a policy for a channel they don't have permissions on, even if they attempt to + // use a policy ID that matches a channel they control. + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.AccessControlSettings.EnableAttributeBasedAccessControl = model.NewPointer(true) + }) + + ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced)) + require.True(t, ok, "SetLicense should return true") + + // Add permission to channel admin role + th.AddPermissionToRole(t, model.PermissionManageChannelAccessRules.Id, model.ChannelAdminRoleId) + + // Create two private channels + channelA := th.CreatePrivateChannel(t) + channelB := th.CreatePrivateChannel(t) + + // Create a channel admin who only has access to channel A + channelAdmin := th.CreateUser(t) + th.LinkUserToTeam(t, channelAdmin, th.BasicTeam) + th.AddUserToChannel(t, channelAdmin, channelA) + th.MakeUserChannelAdmin(t, channelAdmin, channelA) + + // Create a policy for channel B (which the channel admin does NOT have access to) + channelBPolicy := &model.AccessControlPolicy{ + ID: channelB.Id, + Type: model.AccessControlPolicyTypeChannel, + Version: model.AccessControlPolicyVersionV0_2, + Revision: 1, + Rules: []model.AccessControlPolicyRule{ + { + Expression: "user.attributes.team == 'engineering'", + Actions: []string{"*"}, + }, + }, + } + _, err := th.App.Srv().Store().AccessControlPolicy().Save(th.Context, channelBPolicy) + require.NoError(t, err) + + channelAdminClient := th.CreateClient() + _, _, err = channelAdminClient.Login(context.Background(), channelAdmin.Email, channelAdmin.Password) + require.NoError(t, err) + + mockAccessControlService := &mocks.AccessControlServiceInterface{} + th.App.Srv().Channels().AccessControl = mockAccessControlService + mockAccessControlService.On("GetPolicy", mock.AnythingOfType("*request.Context"), channelB.Id).Return(channelBPolicy, nil) + + // Attempt to update the policy for channel B (which the admin doesn't have access to) + maliciousUpdateReq := model.AccessControlPolicyActiveUpdateRequest{ + Entries: []model.AccessControlPolicyActiveUpdate{ + {ID: channelB.Id, Active: true}, + }, + } + + // Channel admin should NOT be able to set active status for another channel's policy + _, resp, err := channelAdminClient.SetAccessControlPolicyActive(context.Background(), maliciousUpdateReq) + require.Error(t, err) + CheckForbiddenStatus(t, resp) + }) }
server/channels/app/access_control.go+0 −9 modified@@ -302,15 +302,6 @@ func (a *App) GetAccessControlFieldsAutocomplete(rctx request.CTX, after string, return fields, nil } -func (a *App) UpdateAccessControlPolicyActive(rctx request.CTX, policyID string, active bool) *model.AppError { - _, err := a.Srv().Store().AccessControlPolicy().SetActiveStatus(rctx, policyID, active) - if err != nil { - return model.NewAppError("UpdateAccessControlPolicyActive", "app.pap.update_access_control_policy_active.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - return nil -} - func (a *App) UpdateAccessControlPoliciesActive(rctx request.CTX, updates []model.AccessControlPolicyActiveUpdate) ([]*model.AccessControlPolicy, *model.AppError) { acs := a.Srv().ch.AccessControl if acs == nil {
server/i18n/en.json+0 −4 modified@@ -6918,10 +6918,6 @@ "id": "app.pap.update_access_control_policies_active.app_error", "translation": "Could not update active status of access control policies." }, - { - "id": "app.pap.update_access_control_policy_active.app_error", - "translation": "Could not change active status of access control policy." - }, { "id": "app.pdp.access_evaluation.app_error", "translation": "Failed evaluate access control policy."
webapp/channels/src/components/admin_console/team_channel_settings/channel/details/channel_details.test.tsx+3 −3 modified@@ -105,7 +105,7 @@ describe('admin_console/team_channel_settings/channel/ChannelDetails', () => { saveChannelAccessPolicy: jest.fn().mockResolvedValue({data: {}}), validateChannelExpression: jest.fn().mockResolvedValue({data: {}}), createAccessControlSyncJob: jest.fn().mockResolvedValue({data: {}}), - updateAccessControlPolicyActive: jest.fn().mockResolvedValue({data: {}}), + updateAccessControlPoliciesActive: jest.fn().mockResolvedValue({data: {}}), searchUsersForExpression: jest.fn().mockResolvedValue({data: {users: [], total: 0}}), getChannelMembers: jest.fn().mockResolvedValue({data: []}), getProfilesByIds: jest.fn().mockResolvedValue({data: []}), @@ -246,7 +246,7 @@ describe('admin_console/team_channel_settings/channel/ChannelDetails', () => { saveChannelAccessPolicy: jest.fn().mockResolvedValue({data: {}}), validateChannelExpression: jest.fn().mockResolvedValue({data: {}}), createAccessControlSyncJob: jest.fn().mockResolvedValue({data: {}}), - updateAccessControlPolicyActive: jest.fn().mockResolvedValue({data: {}}), + updateAccessControlPoliciesActive: jest.fn().mockResolvedValue({data: {}}), searchUsersForExpression: jest.fn().mockResolvedValue({data: {users: [], total: 0}}), getChannelMembers: jest.fn().mockResolvedValue({data: []}), getProfilesByIds: jest.fn().mockResolvedValue({data: []}), @@ -388,7 +388,7 @@ describe('admin_console/team_channel_settings/channel/ChannelDetails', () => { saveChannelAccessPolicy: jest.fn().mockResolvedValue({data: {}}), validateChannelExpression: jest.fn().mockResolvedValue({data: {}}), createAccessControlSyncJob: jest.fn().mockResolvedValue({data: {}}), - updateAccessControlPolicyActive: jest.fn().mockResolvedValue({data: {}}), + updateAccessControlPoliciesActive: jest.fn().mockResolvedValue({data: {}}), searchUsersForExpression: jest.fn().mockResolvedValue({data: {users: [], total: 0}}), getChannelMembers: jest.fn().mockResolvedValue({data: []}), getProfilesByIds: jest.fn().mockResolvedValue({data: []}),
webapp/channels/src/components/admin_console/team_channel_settings/channel/details/channel_details.tsx+3 −3 modified@@ -5,7 +5,7 @@ import cloneDeep from 'lodash/cloneDeep'; import React from 'react'; import {FormattedMessage} from 'react-intl'; -import type {AccessControlPolicy} from '@mattermost/types/access_control'; +import type {AccessControlPolicy, AccessControlPolicyActiveUpdate} from '@mattermost/types/access_control'; import type {Channel, ChannelModeration as ChannelPermissions, ChannelModerationPatch} from '@mattermost/types/channels'; import {SyncableType} from '@mattermost/types/groups'; import type {SyncablePatch, Group} from '@mattermost/types/groups'; @@ -139,7 +139,7 @@ export type ChannelDetailsActions = { saveChannelAccessPolicy: (policy: AccessControlPolicy) => Promise<ActionResult>; validateChannelExpression: (expression: string, channelId: string) => Promise<ActionResult>; createAccessControlSyncJob: (job: JobTypeBase & { data: any }) => Promise<ActionResult>; - updateAccessControlPolicyActive: (policyId: string, active: boolean) => Promise<ActionResult>; + updateAccessControlPoliciesActive: (states: AccessControlPolicyActiveUpdate[]) => Promise<ActionResult>; searchUsersForExpression: (expression: string, term: string, after: string, limit: number, channelId?: string) => Promise<ActionResult>; getChannelMembers: (channelId: string, page?: number, perPage?: number) => Promise<ActionResult>; getProfilesByIds: (userIds: string[]) => Promise<ActionResult>; @@ -802,7 +802,7 @@ export default class ChannelDetails extends React.PureComponent<ChannelDetailsPr } else { // Update the active status separately try { - await actions.updateAccessControlPolicyActive(channelID, channelRulesAutoSync); + await actions.updateAccessControlPoliciesActive([{id: channelID, active: channelRulesAutoSync} as AccessControlPolicyActiveUpdate]); } catch (activeError) { // eslint-disable-next-line no-console console.error('Failed to update policy active status:', activeError);
webapp/channels/src/components/admin_console/team_channel_settings/channel/details/index.ts+2 −2 modified@@ -7,7 +7,7 @@ import type {Dispatch} from 'redux'; import type {GlobalState} from '@mattermost/types/store'; -import {getAccessControlPolicy, deleteAccessControlPolicy, assignChannelsToAccessControlPolicy, searchAccessControlPolicies, unassignChannelsFromAccessControlPolicy, createAccessControlPolicy, getAccessControlFields, getVisualAST, validateExpressionAgainstRequester, updateAccessControlPolicyActive, searchUsersForExpression} from 'mattermost-redux/actions/access_control'; +import {getAccessControlPolicy, deleteAccessControlPolicy, assignChannelsToAccessControlPolicy, searchAccessControlPolicies, unassignChannelsFromAccessControlPolicy, createAccessControlPolicy, getAccessControlFields, getVisualAST, validateExpressionAgainstRequester, updateAccessControlPoliciesActive, searchUsersForExpression} from 'mattermost-redux/actions/access_control'; import { addChannelMember, deleteChannel, @@ -127,7 +127,7 @@ function mapDispatchToProps(dispatch: Dispatch) { saveChannelAccessPolicy: createAccessControlPolicy, validateChannelExpression: validateExpressionAgainstRequester, createAccessControlSyncJob: createJob, - updateAccessControlPolicyActive, + updateAccessControlPoliciesActive, searchUsersForExpression, getChannelMembers, getProfilesByIds,
webapp/channels/src/components/channel_settings_modal/channel_settings_access_rules_activity_warning.test.tsx+1 −2 modified@@ -146,10 +146,9 @@ describe('ChannelSettingsAccessRulesTab - Activity Warning Integration', () => { getChannelMembers: jest.fn().mockResolvedValue({data: []}), createJob: jest.fn().mockResolvedValue({data: {}}), createAccessControlSyncJob: jest.fn().mockResolvedValue({data: {}}), - updateAccessControlPolicyActive: jest.fn().mockResolvedValue({data: {}}), + updateAccessControlPoliciesActive: jest.fn().mockResolvedValue({data: {}}), validateExpressionAgainstRequester: jest.fn().mockResolvedValue({data: {requester_matches: true}}), savePreferences: jest.fn().mockResolvedValue({data: {}}), - updateAccessControlPoliciesActive: jest.fn().mockResolvedValue({data: {}}), }; const defaultProps = {
webapp/channels/src/packages/mattermost-redux/src/actions/access_control.ts+0 −10 modified@@ -125,16 +125,6 @@ export function getAccessControlFields(after: string, limit: number, channelId?: }); } -export function updateAccessControlPolicyActive(policyId: string, active: boolean) { - return bindClientFunc({ - clientFunc: Client4.updateAccessControlPolicyActive, - params: [ - policyId, - active, - ], - }); -} - export function searchUsersForExpression(expression: string, term: string, after: string, limit: number, channelId?: string): ActionFuncAsync<AccessControlTestResult> { return async (dispatch, getState) => { let data;
webapp/platform/client/src/client4.ts+0 −7 modified@@ -4681,13 +4681,6 @@ export default class Client4 { ); }; - updateAccessControlPolicyActive = (policyId: string, active: boolean) => { - return this.doFetch<StatusOK>( - `${this.getBaseRoute()}/access_control_policies/${policyId}/activate?active=${active}`, - {method: 'get'}, - ); - }; - assignChannelsToAccessControlPolicy = (policyId: string, channelIds: string[]) => { return this.doFetch<StatusOK>( `${this.getBaseRoute()}/access_control_policies/${policyId}/assign`,
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- github.com/advisories/GHSA-rv67-7w2g-7976ghsaADVISORY
- mattermost.com/security-updatesghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-22545ghsaADVISORY
- github.com/mattermost/mattermost/commit/ced9a56e3988fe9fd4559d45f9971dbd562e2218ghsaWEB
News mentions
0No linked articles in our index yet.