VYPR
Medium severity6.5NVD Advisory· Published May 18, 2026· Updated May 18, 2026

CVE-2026-6345

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

Patches

1
1f52b0f10a56

[MM-67377] Fix (#35336) (#35530)

https://github.com/mattermost/mattermostMattermost BuildMar 9, 2026Fixed in 11.5.2via llm-release-walk
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

News mentions

0

No linked articles in our index yet.