Skip to content
winfunc
Disclosure record · Mattermost

Private channel enumeration through /mute error messages (CVE-2026-21386)

/mute exposed whether a private channel existed by returning a distinct not-member error

MattermostCVE-2026-21386DisclosedSource record

CVSS 3.1 base score

Medium
4.3/ 10

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

Record

Project
Mattermost
Severity
Medium4.3
CVE
CVE-2026-21386
Disclosed
Trace
3 steps

Summary

Source

MuteProvider.DoCommand

server/channels/app/slashcommands/command_mute.go:48

Sink

MuteProvider.DoCommand

server/channels/app/slashcommands/command_mute.go:65

The /mute slash command accepts an optional channel handle, strips the leading ~, and looks up the named channel with Store().Channel().GetByName(channel.TeamId, channelName, true). That lookup can resolve a private channel by name even when the caller is not a member. If the channel does not exist, the handler returns api.command_mute.error; if it exists but ToggleMuteChannel fails because the caller is not a member, the handler returned api.command_mute.not_member.error. Those two responses gave authenticated users a yes/no oracle for private channel names.

The fix keeps the behavior but normalizes the not-member path to the same generic error used for nonexistent channels, and removes the now-unused translation. The original fix is PR #35099 / commit 5bb5261c72faa476558a694c23581d24b734da41; backports include #35145, #35147, #35148, and #35149.

Severity

Attack vectorAV
Network
Attack complexityAC
Low
Privileges requiredPR
Low
User interactionUI
None
ScopeS
Unchanged
ConfidentialityC
Low
IntegrityI
None
AvailabilityA
None

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/app/slashcommands/command_mute.go:48

    MuteProvider.DoCommand

  2. Step 01server/channels/app/slashcommands/command_mute.go:48-56

    The slash command treats the user-supplied argument as a channel name, supporting both ~channel and bare channel names.

    go
    channelName := ""
    splitMessage := strings.Split(message, " ")
    if strings.HasPrefix(message, "~") {
        channelName = splitMessage[0][1:]
    } else {
        channelName = splitMessage[0]
    }
    
  3. Step 02server/channels/app/slashcommands/command_mute.go:58-63

    The handler looks up the channel by name and returns api.command_mute.error when no channel exists.

    go
    if channelName != "" && message != "" {
        channel, _ = a.Srv().Store().Channel().GetByName(channel.TeamId, channelName, true)
    
        if channel == nil {
            return &model.CommandResponse{Text: args.T("api.command_mute.error", map[string]any{"Channel": channelName}), ResponseType: model.CommandResponseTypeEphemeral}
        }
    }
    
  4. Step 03server/channels/app/slashcommands/command_mute.go:65-68

    Before the fix, an existing channel that the user could not mute returned a different not_member error. The patched code returns the generic nonexistent-channel error instead.

    go
    channelMember, err := a.ToggleMuteChannel(rctx, channel.Id, args.UserId)
    if err != nil {
        return &model.CommandResponse{Text: args.T("api.command_mute.error", map[string]any{"Channel": channelName}), ResponseType: model.CommandResponseTypeEphemeral}
    }
    
  5. Sinkserver/channels/app/slashcommands/command_mute.go:65

    MuteProvider.DoCommand

Impact

Reported impact

The issue leaks the existence of private channels and their names. It does not expose channel messages, but private channel names often reveal incident, customer, project, or organizational information.

Attack surface

The authenticated /mute slash command in any team where users can submit slash commands.

Preconditions

The attacker must be authenticated and able to run slash commands in a team. They do not need to be a member of the private channels they probe.

Attack path

  1. 1

    Run /mute ~candidate-channel from a channel where slash commands are accepted.

  2. 2

    On vulnerable builds, compare the response against a definitely nonexistent channel.

  3. 3

    api.command_mute.not_member.error identifies an existing channel the user is not a member of.

  4. 4

    Automate candidate names to enumerate private channel existence.

Proof of concept

Reproduction5 stages
  1. 01

    Environment setup

    Use a vulnerable build before PR #35099. Create a team, a normal user, and a private channel named secret-ops that the normal user is not a member of.

  2. 02

    Target configuration

    The normal user only needs access to any channel where they can run /mute.

  3. 03

    Exploit delivery

    As the normal user, run /mute ~secret-ops and /mute ~definitely-does-not-exist.

  4. 04

    Expected response

    Vulnerable builds return different translation IDs or messages for existing-not-member versus nonexistent channels. Fixed builds return the same generic api.command_mute.error response.

  5. 05

    Outcome

    The response no longer distinguishes private channel existence from nonexistent channel names.

Remediation

Guidance

Do not expose authorization failures and object-not-found failures as distinguishable messages where object existence is sensitive. Normalize the ToggleMuteChannel failure path to the same generic error used for missing channels.

Before and after−4+4
channelMember, err := a.ToggleMuteChannel(rctx, channel.Id, args.UserId)if err != nil {    return &model.CommandResponse{Text: args.T("api.command_mute.not_member.error", map[string]any{"Channel": channelName}), ResponseType: model.CommandResponseTypeEphemeral}}channelMember, err := a.ToggleMuteChannel(rctx, channel.Id, args.UserId)if err != nil {    return &model.CommandResponse{Text: args.T("api.command_mute.error", map[string]any{"Channel": channelName}), ResponseType: model.CommandResponseTypeEphemeral}}
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.