Group syncable scheme_admin authorization bypass (CVE-2026-7387)
Group-syncable link and patch endpoints accepted role-granting scheme_admin changes under weaker link permissions
CVSS 3.1 base score
HighVector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Summary
Source
linkGroupSyncable()
server/channels/api4/group.go:354
Sink
UpdateMembersRole()
server/channels/store/sqlstore/team_store.go:1617
Mattermost group syncables let LDAP groups be linked to teams or channels and can optionally mark synchronized group members as scheme admins. Before the fix, linkGroupSyncable() and patchGroupSyncable() decoded scheme_admin as ordinary patch data, checked only the permissions needed to link or patch the group syncable, and then persisted the flag. The background sync path later trusted that stored bit and updated team or channel member role state.
The public advisory MMSA-2026-00665 / CVE-2026-7387 describes this as a High CWE-863 issue affecting Mattermost 11.6.x <= 11.6.1, 11.5.x <= 11.5.4, and 10.11.x <= 10.11.16, with fixed versions 11.7.0, 11.6.2, 11.5.5, 10.11.16, and 10.11.17. The original fix is PR #36316 / commit 8c72083414e675c97987374395e36d1f36b4bd8a; release-line backports include #36431, #36432, and #36434.
Severity
- Attack vectorAV
- Network
- Attack complexityAC
- Low
- Privileges requiredPR
- Low
- User interactionUI
- None
- ScopeS
- Unchanged
- ConfidentialityC
- High
- IntegrityI
- High
- AvailabilityA
- High
Metric values as published in the disclosure vector. Meters show how far each value raises exposure.
Source-to-sink trace
- Source · attacker-controlledserver/
channels/ api4/ group.go:354 linkGroupSyncable()
- Step 01server/
channels/ api4/ group.go:354-358 The link endpoint decodes the request body into
GroupSyncablePatch. A caller can includescheme_adminin the same JSON body used for ordinary syncable settings such asauto_add.go var patch *model.GroupSyncablePatch err = json.Unmarshal(body, &patch) if err != nil { c.SetInvalidParamWithErr("group_syncable", err) return } - Step 02server/
public/ model/ group_syncable.go:167-169 The model patch helper copied
SchemeAdmindirectly into the syncable object. It had no authorization context, so it could not distinguish harmless configuration changes from role-granting changes.go if patch.SchemeAdmin != nil { syncable.SchemeAdmin = *patch.SchemeAdmin } - Step 03server/
channels/ api4/ group.go:372-381 (before fix) Before PR #36316, the API persisted the patched object after the weaker link permission checks. The same missing role-management gate existed in the patch endpoint.
go groupSyncable := &model.GroupSyncable{ GroupId: c.Params.GroupId, SyncableId: syncableID, Type: syncableType, } groupSyncable.Patch(patch) groupSyncable, appErr = c.App.UpsertGroupSyncable(groupSyncable) - Step 04server/
channels/ api4/ group.go:387-389 (before fix) Persisting the syncable triggered asynchronous role and membership synchronization. A caller-controlled
SchemeAdminbit therefore flowed from the request body into background role changes.go c.App.Srv().Go(func() { c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, c.Params.GroupId) }) - Step 05server/
channels/ store/ sqlstore/ group_store.go:1832-1836 The sync path asks the store for group members whose linked syncable has
SchemeAdmin = TRUE. That turns the persisted client-controlled field into the list of users who should become admins.go builder := s.getQueryBuilder().Select("UserId"). From(fmt.Sprintf("Group%ss", syncableType)). Join(fmt.Sprintf("GroupMembers ON GroupMembers.GroupId = Group%ss.GroupId AND Group%[1]ss.SchemeAdmin = TRUE AND GroupMembers.DeleteAt = 0", syncableType.String())) - Step 06server/
channels/ store/ sqlstore/ team_store.go:1617-1625 The selected user IDs are written into team membership state as scheme admins. The channel path uses the same pattern for channel members.
go func (s SqlTeamStore) UpdateMembersRole(teamID string, userIDs []string) ([]*model.TeamMember, error) { query := s.getQueryBuilder(). Update("TeamMembers"). Set("SchemeAdmin", sq.Case().When(sq.Eq{"UserId": userIDs}, "true").Else("false")) } - Step 07server/
channels/ api4/ group.go:739-766 (fix, commit 8c72083) The fix gates any explicit
SchemeAdminvalue on the same role-management permissions used by direct role assignment, or on the sysconsole groups-management permission.go func verifySchemeAdminAssignmentPermission(c *Context, syncableType model.GroupSyncableType, syncableID string, patch *model.GroupSyncablePatch) *model.AppError { if patch == nil || patch.SchemeAdmin == nil { return nil } if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementGroups) { return nil } switch syncableType { case model.GroupSyncableTypeTeam: if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), syncableID, model.PermissionManageTeamRoles) { return model.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionManageTeamRoles}) } case model.GroupSyncableTypeChannel: if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), syncableID, model.PermissionManageChannelRoles); !ok { return model.MakePermissionError(c.AppContext.Session(), []*model.Permission{model.PermissionManageChannelRoles}) } } return nil } - Sinkserver/
channels/ store/ sqlstore/ team_store.go:1617 UpdateMembersRole()
Impact
Reported impact
The vulnerability bypasses Mattermost's intended role-management authorization boundary. A caller who should only manage group-sync configuration can cause synchronized group members to become team or channel admins, potentially granting membership management, settings changes, and other administrative abilities in the affected scope.
Attack surface
Mattermost deployments with LDAP groups enabled/licensed where a caller can link or patch group syncables for a team or channel. The affected API surface includes group syncable link and patch routes for teams and channels.
Preconditions
The attacker must be authenticated, know or obtain a referenceable group ID and target team/channel ID, and have the weaker group-link or group-syncable management permissions for that target, but not the stronger role-management permission that direct admin-role assignment requires.
Attack path
- 1
Authenticate as a user who can link or patch a group syncable for a team or channel.
- 2
Send a link or patch request containing
scheme_admin: true. - 3
Wait for
SyncRolesAndMembershipto run. - 4
Confirm that synchronized group members have team or channel admin role state.
Proof of concept
- 01
Environment setup
Use a vulnerable Mattermost build before PR #36316 with LDAP groups enabled/licensed. Prepare one referenceable LDAP group and an authenticated user who can reach the group-syncable link or patch endpoint for the target team or channel but lacks direct role-management permission.
- 02
Target configuration
Set
MM_URL,SESSION,GROUP_ID,TEAM_ID, andUSER_IDfor the target instance. For a channel-scoped test, use the channel syncable endpoint and inspect the corresponding channel member record. - 03
Exploit delivery
Create or update a group syncable while setting
scheme_admin:bash curl -sS -X POST "$MM_URL/api/v4/groups/$GROUP_ID/teams/$TEAM_ID/link" \ -H "Cookie: $SESSION" \ -H 'Content-Type: application/json' \ --data '{"auto_add":true,"scheme_admin":true}' sleep 3 curl -sS "$MM_URL/api/v4/teams/$TEAM_ID/members/$USER_ID" \ -H "Cookie: $SESSION" | jq '.scheme_admin, .roles' - 04
Expected response
Vulnerable builds accept the request and, after synchronization, show
scheme_admin: trueor equivalent admin role state for synchronized members. Fixed builds reject explicitscheme_adminchanges unless the caller has the required role-management permission. - 05
Outcome
The attacker can convert group-sync configuration access into team or channel admin role assignment on vulnerable builds. PR #36316 restores the stronger authorization boundary.
Remediation
Guidance
Treat scheme_admin as role-granting state, not ordinary syncable metadata. Link and patch handlers must reject any explicit SchemeAdmin value unless the caller holds manage_team_roles, manage_channel_roles, or the sysconsole groups-management override. Preserve omitted SchemeAdmin values so ordinary auto_add edits continue to work without demoting existing direct admins.
groupSyncable.Patch(patch)groupSyncable, appErr = c.App.UpsertGroupSyncable(groupSyncable)c.App.Srv().Go(func() {c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, c.Params.GroupId)})appErr = verifySchemeAdminAssignmentPermission(c, syncableType, syncableID, patch)if appErr != nil { appErr.Where = "Api4.patchGroupSyncable" c.Err = appErr return} groupSyncable.Patch(patch)groupSyncable, appErr = c.App.UpsertGroupSyncable(groupSyncable) syncRoles := patch.SchemeAdmin != nilc.App.Srv().Go(func() { c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, c.Params.GroupId, syncRoles)})
Check the upstream record for the project's current remediation status.
Investigate the paths that matter in your codebase.
Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.
