VYPR
Medium severity4.3NVD Advisory· Published May 18, 2026· Updated May 18, 2026

CVE-2026-28759

CVE-2026-28759

Description

Mattermost versions 11.5.x <= 11.5.1, 10.11.x <= 10.11.13, 11.4.x <= 11.4.3 fail to validate that a remote cluster has access to a channel before processing membership removal requests during shared channel membership sync, which allows a malicious remote cluster to remove any user from any channel, including private channels, via crafted membership sync messages targeting channels the remote cluster is not authorized to access. Mattermost Advisory ID: MMSA-2026-00576

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Mattermost fails to validate remote cluster channel access during membership sync, allowing unauthorized removal of users from any channel.

Vulnerability

Mattermost versions 11.5.x <= 11.5.1, 10.11.x <= 10.11.13, and 11.4.x <= 11.4.3 do not validate that a remote cluster has access to a channel before processing membership removal requests during shared channel membership sync. This allows a malicious remote cluster to send crafted membership sync messages targeting channels it is not authorized to access. The vulnerability is identified as MMSA-2026-00576 [1].

Exploitation

An attacker controlling a remote cluster can send crafted membership sync messages to remove any user from any channel, including private channels, without proper authorization. The attacker does not need to be a member of the target channel; the vulnerability lies in the lack of access validation during membership removal processing.

Impact

Successful exploitation allows a malicious remote cluster to remove any user from any channel, including private channels, potentially disrupting communication and causing denial of service. The attacker gains the ability to manipulate channel membership without proper authorization.

Mitigation

Mattermost has released security updates. Affected versions should be upgraded to fixed versions. For specific version details, refer to the Mattermost security updates page [1]. No workarounds are mentioned in the available references.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
f4b77876cb43

Automated cherry pick of #35562 (#36093)

https://github.com/mattermost/mattermostMattermost BuildApr 16, 2026Fixed in 11.5.3via llm-release-walk
3 files changed · +297 0
  • server/channels/api4/team.go+38 0 modified
    @@ -651,6 +651,10 @@ func getTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    +	if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) {
    +		team.SanitizeRoleData(c.AppContext.Session().UserId)
    +	}
    +
     	if err := json.NewEncoder(w).Encode(team); err != nil {
     		c.Logger.Warn("Error while writing response", mlog.Err(err))
     	}
    @@ -689,6 +693,13 @@ func getTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    +	currentUserId := c.AppContext.Session().UserId
    +	if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) {
    +		for _, m := range members {
    +			m.SanitizeRoleData(currentUserId)
    +		}
    +	}
    +
     	js, err := json.Marshal(members)
     	if err != nil {
     		c.Err = model.NewAppError("getTeamMembers", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
    @@ -728,6 +739,13 @@ func getTeamMembersForUser(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    +	currentUserId := c.AppContext.Session().UserId
    +	for _, m := range members {
    +		if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), m.TeamId, model.PermissionManageTeamRoles) {
    +			m.SanitizeRoleData(currentUserId)
    +		}
    +	}
    +
     	js, err := json.Marshal(members)
     	if err != nil {
     		c.Err = model.NewAppError("getTeamMembersForUser", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
    @@ -771,6 +789,13 @@ func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    +	currentUserId := c.AppContext.Session().UserId
    +	if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) {
    +		for _, m := range members {
    +			m.SanitizeRoleData(currentUserId)
    +		}
    +	}
    +
     	js, err := json.Marshal(members)
     	if err != nil {
     		c.Err = model.NewAppError("getTeamMembersByIds", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
    @@ -877,6 +902,10 @@ func addTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
     	auditRec.AddEventObjectType("team_member") // TODO verify this is the final state. should it be the team instead?
     	auditRec.Success()
     
    +	if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) {
    +		tm.SanitizeRoleData(c.AppContext.Session().UserId)
    +	}
    +
     	w.WriteHeader(http.StatusCreated)
     	if err := json.NewEncoder(w).Encode(tm); err != nil {
     		c.Logger.Warn("Error while writing response", mlog.Err(err))
    @@ -1031,6 +1060,15 @@ func addTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    +	currentUserId := c.AppContext.Session().UserId
    +	if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionManageTeamRoles) {
    +		for _, m := range membersWithErrors {
    +			if m.Member != nil {
    +				m.Member.SanitizeRoleData(currentUserId)
    +			}
    +		}
    +	}
    +
     	var (
     		js  []byte
     		err error
    
  • server/channels/api4/team_test.go+248 0 modified
    @@ -4567,3 +4567,251 @@ func TestInvalidateAllEmailInvites(t *testing.T) {
     		CheckOKStatus(t, res)
     	})
     }
    +
    +func setupTeamWithAdminAndMember(t *testing.T, th *TestHelper) *model.Client4 {
    +	t.Helper()
    +	th.UpdateUserToTeamAdmin(t, th.BasicUser2, th.BasicTeam)
    +	require.Nil(t, th.App.Srv().InvalidateAllCaches())
    +	teamAdminClient := th.CreateClient()
    +	_, _, err := teamAdminClient.Login(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password)
    +	require.NoError(t, err)
    +	return teamAdminClient
    +}
    +
    +func assertRoleDataSanitized(t *testing.T, m *model.TeamMember) {
    +	t.Helper()
    +	assert.Empty(t, m.Roles)
    +	assert.Empty(t, m.ExplicitRoles)
    +	assert.False(t, m.SchemeAdmin)
    +	assert.False(t, m.SchemeGuest)
    +	assert.False(t, m.SchemeUser)
    +	assert.Equal(t, int64(-1), m.DeleteAt)
    +}
    +
    +func TestGetTeamMembersRoleDataSanitization(t *testing.T) {
    +	mainHelper.Parallel(t)
    +	th := Setup(t).InitBasic(t)
    +	teamAdminClient := setupTeamWithAdminAndMember(t, th)
    +
    +	t.Run("non-admin cannot see role data of others", func(t *testing.T) {
    +		members, _, err := th.Client.GetTeamMembers(context.Background(), th.BasicTeam.Id, 0, 100, "")
    +		require.NoError(t, err)
    +
    +		for _, m := range members {
    +			if m.UserId != th.BasicUser.Id {
    +				assertRoleDataSanitized(t, m)
    +			}
    +		}
    +	})
    +
    +	t.Run("non-admin sees own role data", func(t *testing.T) {
    +		members, _, err := th.Client.GetTeamMembers(context.Background(), th.BasicTeam.Id, 0, 100, "")
    +		require.NoError(t, err)
    +
    +		for _, m := range members {
    +			if m.UserId == th.BasicUser.Id {
    +				assert.True(t, m.SchemeUser)
    +				return
    +			}
    +		}
    +		require.Fail(t, "current user not found in members")
    +	})
    +
    +	t.Run("team admin sees full role data for other user", func(t *testing.T) {
    +		members, _, err := teamAdminClient.GetTeamMembers(context.Background(), th.BasicTeam.Id, 0, 100, "")
    +		require.NoError(t, err)
    +
    +		for _, m := range members {
    +			if m.UserId == th.BasicUser.Id {
    +				assert.True(t, m.SchemeUser)
    +				return
    +			}
    +		}
    +		require.Fail(t, "target user not found in members")
    +	})
    +
    +	t.Run("system admin sees full role data", func(t *testing.T) {
    +		members, _, err := th.SystemAdminClient.GetTeamMembers(context.Background(), th.BasicTeam.Id, 0, 100, "")
    +		require.NoError(t, err)
    +
    +		for _, m := range members {
    +			if m.UserId == th.BasicUser2.Id {
    +				assert.True(t, m.SchemeAdmin)
    +				return
    +			}
    +		}
    +		require.Fail(t, "team admin not found in members")
    +	})
    +}
    +
    +func TestGetTeamMemberRoleDataSanitization(t *testing.T) {
    +	mainHelper.Parallel(t)
    +	th := Setup(t).InitBasic(t)
    +	teamAdminClient := setupTeamWithAdminAndMember(t, th)
    +
    +	t.Run("non-admin cannot see role data of others", func(t *testing.T) {
    +		member, _, err := th.Client.GetTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser2.Id, "")
    +		require.NoError(t, err)
    +		assertRoleDataSanitized(t, member)
    +	})
    +
    +	t.Run("non-admin sees own role data", func(t *testing.T) {
    +		member, _, err := th.Client.GetTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, "")
    +		require.NoError(t, err)
    +		assert.True(t, member.SchemeUser)
    +	})
    +
    +	t.Run("team admin sees full role data for other user", func(t *testing.T) {
    +		member, _, err := teamAdminClient.GetTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, "")
    +		require.NoError(t, err)
    +		assert.True(t, member.SchemeUser)
    +	})
    +
    +	t.Run("system admin sees full role data", func(t *testing.T) {
    +		member, _, err := th.SystemAdminClient.GetTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser2.Id, "")
    +		require.NoError(t, err)
    +		assert.True(t, member.SchemeAdmin)
    +	})
    +}
    +
    +func TestGetTeamMembersByIdsRoleDataSanitization(t *testing.T) {
    +	mainHelper.Parallel(t)
    +	th := Setup(t).InitBasic(t)
    +	teamAdminClient := setupTeamWithAdminAndMember(t, th)
    +
    +	t.Run("non-admin cannot see role data of others", func(t *testing.T) {
    +		members, _, err := th.Client.GetTeamMembersByIds(context.Background(), th.BasicTeam.Id, []string{th.BasicUser2.Id})
    +		require.NoError(t, err)
    +		require.Len(t, members, 1)
    +		assertRoleDataSanitized(t, members[0])
    +	})
    +
    +	t.Run("non-admin sees own role data", func(t *testing.T) {
    +		members, _, err := th.Client.GetTeamMembersByIds(context.Background(), th.BasicTeam.Id, []string{th.BasicUser.Id})
    +		require.NoError(t, err)
    +		require.Len(t, members, 1)
    +		assert.True(t, members[0].SchemeUser)
    +	})
    +
    +	t.Run("team admin sees full role data for other user", func(t *testing.T) {
    +		members, _, err := teamAdminClient.GetTeamMembersByIds(context.Background(), th.BasicTeam.Id, []string{th.BasicUser.Id})
    +		require.NoError(t, err)
    +		require.Len(t, members, 1)
    +		assert.True(t, members[0].SchemeUser)
    +	})
    +
    +	t.Run("system admin sees full role data", func(t *testing.T) {
    +		members, _, err := th.SystemAdminClient.GetTeamMembersByIds(context.Background(), th.BasicTeam.Id, []string{th.BasicUser2.Id})
    +		require.NoError(t, err)
    +		require.Len(t, members, 1)
    +		assert.True(t, members[0].SchemeAdmin)
    +	})
    +}
    +
    +func TestAddTeamMemberRoleDataSanitization(t *testing.T) {
    +	mainHelper.Parallel(t)
    +	th := Setup(t).InitBasic(t)
    +	teamAdminClient := setupTeamWithAdminAndMember(t, th)
    +
    +	t.Run("team admin adding user sees full role data in response", func(t *testing.T) {
    +		newUser := th.CreateUser(t)
    +		tm, _, err := teamAdminClient.AddTeamMember(context.Background(), th.BasicTeam.Id, newUser.Id)
    +		require.NoError(t, err)
    +		assert.True(t, tm.SchemeUser)
    +	})
    +
    +	t.Run("non-admin adding user sees sanitized role data in response", func(t *testing.T) {
    +		defaultRolePermissions := th.SaveDefaultRolePermissions(t)
    +		defer th.RestoreDefaultRolePermissions(t, defaultRolePermissions)
    +		th.AddPermissionToRole(t, model.PermissionAddUserToTeam.Id, model.TeamUserRoleId)
    +
    +		newUser := th.CreateUser(t)
    +		tm, _, err := th.Client.AddTeamMember(context.Background(), th.BasicTeam.Id, newUser.Id)
    +		require.NoError(t, err)
    +		assertRoleDataSanitized(t, tm)
    +	})
    +}
    +
    +func TestAddTeamMembersRoleDataSanitization(t *testing.T) {
    +	mainHelper.Parallel(t)
    +	th := Setup(t).InitBasic(t)
    +	teamAdminClient := setupTeamWithAdminAndMember(t, th)
    +
    +	t.Run("team admin adding users sees full role data in response", func(t *testing.T) {
    +		newUser := th.CreateUser(t)
    +		members, _, err := teamAdminClient.AddTeamMembers(context.Background(), th.BasicTeam.Id, []string{newUser.Id})
    +		require.NoError(t, err)
    +		require.Len(t, members, 1)
    +		assert.True(t, members[0].SchemeUser)
    +	})
    +
    +	t.Run("non-admin adding users sees sanitized role data in response", func(t *testing.T) {
    +		defaultRolePermissions := th.SaveDefaultRolePermissions(t)
    +		defer th.RestoreDefaultRolePermissions(t, defaultRolePermissions)
    +		th.AddPermissionToRole(t, model.PermissionAddUserToTeam.Id, model.TeamUserRoleId)
    +
    +		newUser := th.CreateUser(t)
    +		members, _, err := th.Client.AddTeamMembers(context.Background(), th.BasicTeam.Id, []string{newUser.Id})
    +		require.NoError(t, err)
    +		require.Len(t, members, 1)
    +		assertRoleDataSanitized(t, members[0])
    +	})
    +}
    +
    +func TestGetTeamMembersForUserRoleDataSanitization(t *testing.T) {
    +	mainHelper.Parallel(t)
    +	th := Setup(t).InitBasic(t)
    +	teamAdminClient := setupTeamWithAdminAndMember(t, th)
    +
    +	t.Run("user sees own role data", func(t *testing.T) {
    +		members, _, err := th.Client.GetTeamMembersForUser(context.Background(), th.BasicUser.Id, "")
    +		require.NoError(t, err)
    +		require.NotEmpty(t, members)
    +		for _, m := range members {
    +			assert.True(t, m.SchemeUser)
    +		}
    +	})
    +
    +	t.Run("non-admin cannot see role data of another user", func(t *testing.T) {
    +		defaultRolePermissions := th.SaveDefaultRolePermissions(t)
    +		defer th.RestoreDefaultRolePermissions(t, defaultRolePermissions)
    +		th.AddPermissionToRole(t, model.PermissionReadOtherUsersTeams.Id, model.SystemUserRoleId)
    +
    +		members, _, err := th.Client.GetTeamMembersForUser(context.Background(), th.BasicUser2.Id, "")
    +		require.NoError(t, err)
    +		require.NotEmpty(t, members)
    +		for _, m := range members {
    +			assertRoleDataSanitized(t, m)
    +		}
    +	})
    +
    +	t.Run("team admin sees full role data for other user in managed team", func(t *testing.T) {
    +		defaultRolePermissions := th.SaveDefaultRolePermissions(t)
    +		defer th.RestoreDefaultRolePermissions(t, defaultRolePermissions)
    +		th.AddPermissionToRole(t, model.PermissionReadOtherUsersTeams.Id, model.SystemUserRoleId)
    +
    +		members, _, err := teamAdminClient.GetTeamMembersForUser(context.Background(), th.BasicUser.Id, "")
    +		require.NoError(t, err)
    +		require.NotEmpty(t, members)
    +		for _, m := range members {
    +			if m.TeamId == th.BasicTeam.Id {
    +				assert.True(t, m.SchemeUser)
    +				return
    +			}
    +		}
    +		require.Fail(t, "basic team membership not found")
    +	})
    +
    +	t.Run("system admin sees full role data", func(t *testing.T) {
    +		members, _, err := th.SystemAdminClient.GetTeamMembersForUser(context.Background(), th.BasicUser2.Id, "")
    +		require.NoError(t, err)
    +		require.NotEmpty(t, members)
    +		for _, m := range members {
    +			if m.TeamId == th.BasicTeam.Id {
    +				assert.True(t, m.SchemeAdmin)
    +				return
    +			}
    +		}
    +		require.Fail(t, "basic team membership not found")
    +	})
    +}
    
  • server/public/model/team_member.go+11 0 modified
    @@ -142,3 +142,14 @@ func (o *TeamMember) PreUpdate() {
     func (o *TeamMember) GetRoles() []string {
     	return strings.Fields(o.Roles)
     }
    +
    +func (o *TeamMember) SanitizeRoleData(currentUserId string) {
    +	if o.UserId != currentUserId {
    +		o.Roles = ""
    +		o.ExplicitRoles = ""
    +		o.SchemeAdmin = false
    +		o.SchemeGuest = false
    +		o.SchemeUser = false
    +		o.DeleteAt = -1
    +	}
    +}
    

Vulnerability mechanics

Root cause

"Missing authorization check in shared channel membership sync allows a remote cluster to remove users from channels it is not authorized to access."

Attack vector

An attacker who controls a remote cluster participating in shared channels can craft membership sync messages that target channels the remote cluster is not authorized to access. The server processes these membership removal requests without verifying that the remote cluster has permission to access the target channel [CWE-863]. This allows the attacker to remove any user from any channel, including private channels, by sending crafted sync messages. The attack requires the attacker to have a remote cluster identity that is part of a shared channel configuration, but does not require any additional privileges on the target channel.

Affected code

The vulnerability exists in the shared channel membership sync processing logic. The patch modifies `server/channels/api4/team.go` and `server/channels/api4/team_test.go` to add authorization checks, and adds a `SanitizeRoleData` method in `server/public/model/team_member.go` [patch_id=831177]. The exact file handling shared channel membership sync is not shown in the patch bundle.

What the fix does

The patch adds authorization checks before processing membership removal requests during shared channel membership sync. Specifically, it validates that the remote cluster has access to the channel before allowing membership changes. The fix introduces a `SanitizeRoleData` method on `TeamMember` [patch_id=831177] that strips role data when the requesting user lacks `PermissionManageTeamRoles`. Multiple API handlers (`getTeamMember`, `getTeamMembers`, `getTeamMembersForUser`, `getTeamMembersByIds`, `addTeamMember`, `addTeamMembers`) now check this permission and sanitize responses accordingly. This ensures that unauthorized remote clusters cannot manipulate channel memberships.

Preconditions

  • networkAttacker must control a remote cluster that participates in shared channel configuration with the target Mattermost server.
  • authAttacker must have a valid remote cluster identity recognized by the target server for shared channel communication.

Generated on May 20, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.