← Findings
Mattermost2026CVE-2026-21386

Private Channel Enumeration via /mute Slash Command (CVE-2026-21386)

Summary

The /mute slash command returned different error messages for nonexistent channels versus private channels the user was not a member of, allowing authenticated users to enumerate private channels

The MuteProvider.DoCommand handler allows a user to specify ~channel-handle to toggle the mute state of any channel. Instead of using an authorization-aware helper, it directly calls Store().Channel().GetByName() which returns private channel metadata without verifying membership. The command then returns two different localized errors: api.command_mute.error when a channel truly does not exist, and api.command_mute.not_member.error when ToggleMuteChannel fails because the caller is not a member. This lets any authenticated team member probe arbitrary handles (/mute ~secret-ops) to distinguish nonexistent channels from private channels they are not authorized to know about, leaking channel existence and names.

CVSS

VectorNComplexityLPrivilegesLUser interactionNScopeUConfidentialityLIntegrityNAvailabilityN

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

Source → Sink

Source

server/channels/app/slashcommands/command_mute.go

Line 48 · (*MuteProvider).DoCommand

Sink

server/channels/app/slashcommands/command_mute.go

Line 65 · (*MuteProvider).DoCommand

Attack surface

Any authenticated user in a team can issue slash commands. The /mute command with ~channelname arguments is the attack vector.

Preconditions

Attacker must be an authenticated user in the team but does not need any special role. Only requires knowledge of the team ID and membership in some channel to run slash commands.

Impact

Attackers can enumerate all private channel names within a team, revealing sensitive project or incident names that should be hidden. While no content is exposed, the information leakage can facilitate targeted social engineering and reconnaissance. The attack is low complexity and can be automated with no throttling.

Exploit path

How the issue forms.

01server/channels/app/slashcommands/command_mute.go:58

User-supplied channelName is looked up via the raw store, which returns private channels without membership checks.

GO
channel, _ = a.Srv().Store().Channel().GetByName(channel.TeamId, channelName, true)
02server/channels/app/slashcommands/command_mute.go:60

Nonexistent channels return a distinct error message.

GO
if channel == nil { return ... api.command_mute.error }
03server/channels/app/slashcommands/command_mute.go:65

When the channel exists but user is not a member, a different error string is returned, enabling enumeration.

GO
channelMember, err := a.ToggleMuteChannel(...); if err != nil { return ... api.command_mute.not_member.error }

Proof of concept

Reproduction.

01

Environment

Requirements: Ubuntu 22.04 or macOS 14.

Install dependencies:

BASH
sudo apt update && sudo apt install -y make git go curl

Clone and build Mattermost:

BASH
git clone https://github.com/mattermost/mattermost.git
cd mattermost
make build-server
cd server
./bin/mattermost

Server listens on http://localhost:8065.

02

Configuration

  1. Create an admin account and a test team via the web UI.
  2. Sign in as an ordinary member ([email protected]).
  3. Ensure a private channel named secret-ops exists in the team where the user is not a member.
03

Delivery

Use the slash command input box:

  1. In any channel, run /mute ~secret-ops.
  2. Observe the response api.command_mute.not_member.error.
  3. Run /mute ~doesnotexist and observe api.command_mute.error.

Alternatively, using the REST API:

BASH
curl -X POST http://localhost:8065/api/v4/commands/execute \
  -H 'Authorization: Bearer <user-token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "channel_id": "<current-channel-id>",
    "team_id": "<team-id>",
    "command": "/mute ~secret-ops"
  }'

Log responses:

  • api.command_mute.not_member.error → private channel exists
  • api.command_mute.error → channel absent
04

Outcome

Attacker enumerates names of private channels without authorization, leaking sensitive project names.

Remediation

The fix.

Return the same error message in both cases to prevent private channel discovery. The fix changes the ToggleMuteChannel error path to return api.command_mute.error (the same generic error used for nonexistent channels) instead of the distinct api.command_mute.not_member.error, and removes the now-unused translation key.

Before

TEXT
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}
}

After

TEXT
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}
}

Continue

If this is the bar, see the product.

The archive is public. The product makes it repeatable.

View findings