CVE-2026-6345
Description
Mattermost versions 11.5.x <= 11.5.1, 10.11.x <= 10.11.13, 11.4.x <= 11.4.3 fail prevent disclosure of created user password which allows a malicious attacker to impersonate a user via the use of some of those passwords.. Mattermost Advisory ID: MMSA-2026-00614
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Mattermost versions before 11.5.2, 10.11.14, and 11.4.4 disclose created user passwords, allowing attackers to impersonate users.
The vulnerability affects Mattermost versions 11.5.x up to 11.5.1, 10.11.x up to 10.11.13, and 11.4.x up to 11.4.3. In these versions, the application fails to prevent the disclosure of passwords that are created for new users, potentially exposing them to unauthorized parties.
An attacker who obtains a disclosed password can use it to impersonate the corresponding user. The exact attack vector is not specified, but the disclosure may occur through logs, error messages, or other response mechanisms.
Successful impersonation grants the attacker the victim's privileges, enabling unauthorized access to data, messages, and actions within the Mattermost instance. This can lead to further compromise and data breaches.
Mattermost has released security updates to fix this issue. Users should upgrade to versions 11.5.2, 10.11.14, or 11.4.4 or later. [1]
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: 11.5.x <= 11.5.1, 10.11.x <= 10.11.13, 11.4.x <= 11.4.3
Patches
11f52b0f10a56[MM-67377] Fix (#35336) (#35530)
8 files changed · +181 −17
server/channels/api4/command_test.go+78 −0 modified@@ -325,6 +325,52 @@ func TestUpdateCommand(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) }) + + t.Run("CannotUpdateCommandToDuplicateCustomTrigger", func(t *testing.T) { + cmdA := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: team.Id, + URL: "http://nowhere.com/a", + Method: model.CommandMethodPost, + Trigger: "duplicate_custom_a", + } + createdCmdA, appErr := th.App.CreateCommand(cmdA) + require.Nil(t, appErr) + + cmdB := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: team.Id, + URL: "http://nowhere.com/b", + Method: model.CommandMethodPost, + Trigger: "duplicate_custom_b", + } + createdCmdB, appErr := th.App.CreateCommand(cmdB) + require.Nil(t, appErr) + + createdCmdB.Trigger = createdCmdA.Trigger + _, resp, err := th.Client.UpdateCommand(context.Background(), createdCmdB) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + CheckErrorID(t, err, "api.command.duplicate_trigger.app_error") + }) + + t.Run("CannotUpdateCommandToBuiltInTrigger", func(t *testing.T) { + cmd := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: team.Id, + URL: "http://nowhere.com/c", + Method: model.CommandMethodPost, + Trigger: "custom_for_builtin_collision", + } + createdCmd, appErr := th.App.CreateCommand(cmd) + require.Nil(t, appErr) + + createdCmd.Trigger = "join" + _, resp, err := th.Client.UpdateCommand(context.Background(), createdCmd) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + CheckErrorID(t, err, "api.command.duplicate_trigger.app_error") + }) } func TestMoveCommand(t *testing.T) { @@ -511,6 +557,38 @@ func TestMoveCommand(t *testing.T) { notMovedCmd, _ := th.App.GetCommand(rcmd.Id) require.Equal(t, team.Id, notMovedCmd.TeamId) }) + + t.Run("CannotMoveCommandToTeamWithDuplicateTrigger", func(t *testing.T) { + trigger := "move_duplicate_trigger" + + sourceCmd := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: team.Id, + URL: "http://nowhere.com/source", + Method: model.CommandMethodPost, + Trigger: trigger, + } + sourceCreatedCmd, appErr := th.App.CreateCommand(sourceCmd) + require.Nil(t, appErr) + + targetCmd := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: newTeam.Id, + URL: "http://nowhere.com/target", + Method: model.CommandMethodPost, + Trigger: trigger, + } + _, appErr = th.App.CreateCommand(targetCmd) + require.Nil(t, appErr) + + resp, err := th.Client.MoveCommand(context.Background(), newTeam.Id, sourceCreatedCmd.Id) + require.Error(t, err) + CheckBadRequestStatus(t, resp) + CheckErrorID(t, err, "api.command.duplicate_trigger.app_error") + + notMovedCmd, _ := th.App.GetCommand(sourceCreatedCmd.Id) + require.Equal(t, team.Id, notMovedCmd.TeamId) + }) } func TestDeleteCommand(t *testing.T) {
server/channels/app/command.go+34 −16 modified@@ -693,22 +693,8 @@ func (a *App) CreateCommand(cmd *model.Command) (*model.Command, *model.AppError func (a *App) createCommand(cmd *model.Command) (*model.Command, *model.AppError) { cmd.Trigger = strings.ToLower(cmd.Trigger) - teamCmds, err := a.Srv().Store().Command().GetByTeam(cmd.TeamId) - if err != nil { - return nil, model.NewAppError("CreateCommand", "app.command.createcommand.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - - for _, existingCommand := range teamCmds { - if cmd.Trigger == existingCommand.Trigger { - return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest) - } - } - - for _, builtInProvider := range commandProviders { - builtInCommand := builtInProvider.GetCommand(a, i18n.T) - if builtInCommand != nil && cmd.Trigger == builtInCommand.Trigger { - return nil, model.NewAppError("CreateCommand", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest) - } + if appErr := a.validateCommandTriggerUniqueness(cmd.TeamId, cmd.Trigger, ""); appErr != nil { + return nil, appErr } command, nErr := a.Srv().Store().Command().Save(cmd) @@ -725,6 +711,30 @@ func (a *App) createCommand(cmd *model.Command) (*model.Command, *model.AppError return command, nil } +func (a *App) validateCommandTriggerUniqueness(teamID, trigger, excludeCommandID string) *model.AppError { + trigger = strings.ToLower(trigger) + + for _, builtInProvider := range commandProviders { + builtInCommand := builtInProvider.GetCommand(a, i18n.T) + if builtInCommand != nil && trigger == strings.ToLower(builtInCommand.Trigger) { + return model.NewAppError("validateCommandTriggerUniqueness", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest) + } + } + + teamCmds, err := a.Srv().Store().Command().GetByTeam(teamID) + if err != nil { + return model.NewAppError("validateCommandTriggerUniqueness", "app.command.validatecommandtriggeruniqueness.internal_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + + for _, existingCommand := range teamCmds { + if existingCommand.Id != excludeCommandID && trigger == strings.ToLower(existingCommand.Trigger) { + return model.NewAppError("validateCommandTriggerUniqueness", "api.command.duplicate_trigger.app_error", nil, "", http.StatusBadRequest) + } + } + + return nil +} + func (a *App) GetCommand(commandID string) (*model.Command, *model.AppError) { if !*a.Config().ServiceSettings.EnableCommands { return nil, model.NewAppError("GetCommand", "api.command.disabled.app_error", nil, "", http.StatusNotImplemented) @@ -758,6 +768,10 @@ func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, updatedCmd.PluginId = oldCmd.PluginId updatedCmd.TeamId = oldCmd.TeamId + if appErr := a.validateCommandTriggerUniqueness(updatedCmd.TeamId, updatedCmd.Trigger, updatedCmd.Id); appErr != nil { + return nil, appErr + } + command, err := a.Srv().Store().Command().Update(updatedCmd) if err != nil { var nfErr *store.ErrNotFound @@ -776,6 +790,10 @@ func (a *App) UpdateCommand(oldCmd, updatedCmd *model.Command) (*model.Command, } func (a *App) MoveCommand(team *model.Team, command *model.Command) *model.AppError { + if appErr := a.validateCommandTriggerUniqueness(team.Id, command.Trigger, command.Id); appErr != nil { + return appErr + } + command.TeamId = team.Id _, err := a.Srv().Store().Command().Update(command)
server/channels/app/plugin_api.go+4 −0 modified@@ -1329,6 +1329,10 @@ func (api *PluginAPI) UpdateCommand(commandID string, updatedCmd *model.Command) updatedCmd.TeamId = oldCmd.TeamId } + if appErr := api.app.validateCommandTriggerUniqueness(updatedCmd.TeamId, updatedCmd.Trigger, updatedCmd.Id); appErr != nil { + return nil, appErr + } + return api.app.Srv().Store().Command().Update(updatedCmd) }
server/channels/app/plugin_api_test.go+17 −0 modified@@ -2651,6 +2651,23 @@ func TestPluginAPIUpdateCommand(t *testing.T) { require.NoError(t, appErr) require.Equal(t, "anothernewtriggeragain", newCmd4.Trigger) require.Equal(t, team1.Id, newCmd4.TeamId) + + // Updating a command's trigger to one that already exists should fail. + cmd2 := &model.Command{ + TeamId: team1.Id, + Trigger: "uniquetrigger", + Method: "G", + URL: "http://test.com/uniquetrigger", + } + cmd2, appErr = api.CreateCommand(cmd2) + require.NoError(t, appErr) + + cmd2.Trigger = "anotherNewTriggerAgain" + _, appErr = api.UpdateCommand(cmd2.Id, cmd2) + require.Error(t, appErr) + var appError *model.AppError + require.ErrorAs(t, appErr, &appError) + require.Equal(t, "api.command.duplicate_trigger.app_error", appError.Id) } func TestPluginAPIIsEnterpriseReady(t *testing.T) {
server/channels/app/slashcommands/command_test.go+15 −0 modified@@ -64,6 +64,21 @@ func TestMoveCommand(t *testing.T) { retrievedCommand, err = th.App.GetCommand(command.Id) assert.Nil(t, err) assert.EqualValues(t, targetTeam.Id, retrievedCommand.TeamId) + + // Move a command to a team where the trigger already exists should fail. + command2 := &model.Command{} + command2.CreatorId = model.NewId() + command2.Method = model.CommandMethodPost + command2.TeamId = sourceTeam.Id + command2.URL = "http://nowhere.com/" + command2.Trigger = "trigger1" + + command2, err = th.App.CreateCommand(command2) + assert.Nil(t, err) + + moveErr := th.App.MoveCommand(targetTeam, command2) + assert.NotNil(t, moveErr) + assert.Equal(t, "api.command.duplicate_trigger.app_error", moveErr.Id) } func TestCreateCommandPost(t *testing.T) {
server/i18n/en.json+4 −0 modified@@ -5414,6 +5414,10 @@ "id": "app.command.updatecommand.internal_error", "translation": "Unable to update the command." }, + { + "id": "app.command.validatecommandtriggeruniqueness.internal_error", + "translation": "The specified trigger keyword already exists." + }, { "id": "app.command_webhook.create_command_webhook.existing", "translation": "You cannot update an existing CommandWebhook."
server/public/model/command.go+4 −1 modified@@ -5,6 +5,7 @@ package model import ( "net/http" + "regexp" "strings" ) @@ -15,6 +16,8 @@ const ( MaxTriggerLength = 128 ) +var validCommandTriggerChars = regexp.MustCompile(`^[A-Za-z0-9_./-]+$`) + type Command struct { Id string `json:"id"` Token string `json:"token"` @@ -96,7 +99,7 @@ func (o *Command) IsValid() *AppError { return NewAppError("Command.IsValid", "model.command.is_valid.team_id.app_error", nil, "", http.StatusBadRequest) } - if len(o.Trigger) < MinTriggerLength || len(o.Trigger) > MaxTriggerLength || strings.Index(o.Trigger, "/") == 0 || strings.Contains(o.Trigger, " ") { + if len(o.Trigger) < MinTriggerLength || len(o.Trigger) > MaxTriggerLength || strings.Index(o.Trigger, "/") == 0 || !validCommandTriggerChars.MatchString(o.Trigger) { return NewAppError("Command.IsValid", "model.command.is_valid.trigger.app_error", nil, "", http.StatusBadRequest) }
server/public/model/command_test.go+25 −0 modified@@ -72,6 +72,31 @@ func TestCommandIsValid(t *testing.T) { o.Trigger = strings.Repeat("1", 128) require.Nil(t, o.IsValid()) + validTriggers := []string{"abc", "ABC", "abc123", "a_b-c.d/e"} + for _, trigger := range validTriggers { + o.Trigger = trigger + require.Nil(t, o.IsValid(), "trigger should be valid: %q", trigger) + } + + invalidTriggers := []string{ + " trigger", + "tri gger", + "tri\tger", + "tri\nger", + "tri\rger", + "tri\x00ger", + "/trigger", + "tri?ger", + "tri*ger", + "trígger", + "trigger😀", + } + for _, trigger := range invalidTriggers { + o.Trigger = trigger + require.NotNil(t, o.IsValid(), "trigger should be invalid: %q", trigger) + } + + o.Trigger = "trigger" o.URL = "" require.NotNil(t, o.IsValid(), "should be invalid")
Vulnerability mechanics
Root cause
"Missing validation of command trigger uniqueness on UpdateCommand and MoveCommand operations allows duplicate triggers, and insufficient trigger character validation permits malformed triggers."
Attack vector
An attacker with high privileges (e.g., system admin or team admin) can update an existing slash command's trigger to match another command's trigger on the same team, or move a command to a team where the trigger already exists. The advisory does not specify the exact mechanism for password disclosure, but the duplicate trigger condition could be leveraged to intercept or redirect command execution. The attack is network-based (AV:N) with low complexity (AC:L) and requires high privileges (PR:H).
Affected code
The vulnerability exists in `server/channels/app/command.go` where `UpdateCommand` and `MoveCommand` lacked trigger uniqueness validation before calling the store layer. The `createCommand` function had inline duplicate-check logic that was not shared with update/move paths. Additionally, `server/public/model/command.go` had weak trigger validation using only `strings.Index` and `strings.Contains` checks.
What the fix does
The patch introduces a new helper function `validateCommandTriggerUniqueness` [patch_id=918504] that checks both built-in command triggers and existing team commands for trigger collisions, with an `excludeCommandID` parameter to allow the command being updated to keep its own trigger. This function is called from `UpdateCommand`, `MoveCommand`, `PluginAPI.UpdateCommand`, and the existing `createCommand` path. Additionally, the trigger character validation in `Command.IsValid` is tightened from a simple space/leading-slash check to a regex `^[A-Za-z0-9_./-]+$` [patch_id=918504], rejecting whitespace, control characters, non-ASCII characters, and other invalid characters.
Preconditions
- authAttacker must have high privileges (system admin or team admin) to update or move commands
- configA target command must already exist on the team with a trigger that the attacker wants to duplicate
- networkNetwork access to the Mattermost API endpoints for updating or moving commands
Generated on May 20, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1- mattermost.com/security-updatesnvdVendor Advisory
News mentions
0No linked articles in our index yet.