VYPR
Medium severity4.3NVD Advisory· Published May 18, 2026· Updated May 19, 2026

CVE-2026-28732

CVE-2026-28732

Description

Mattermost versions 11.5.x <= 11.5.1, 10.11.x <= 10.11.13, 11.4.x <= 11.4.3 Fail to enforce slash command trigger-word uniqueness during command updates which allows an authenticated team member with Manage Own Slash Commands permission to hijack and impersonate existing system or custom slash commands via editing their own slash command trigger to an already-registered trigger through the command update API. Mattermost Advisory ID: MMSA-2026-00597

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Authenticated Mattermost team members with Manage Own Slash Commands permission can hijack existing system or custom slash commands due to a missing uniqueness check during command updates.

Vulnerability

Mattermost versions 11.5.x up to and including 11.5.1, 10.11.x up to and including 10.11.13, and 11.4.x up to and including 11.4.3 fail to enforce slash command trigger-word uniqueness when a user updates a slash command via the API. An authenticated team member with the Manage Own Slash Commands permission can set their own command's trigger to a word already registered by another command, including system-level commands. This is described in MMSA-2026-00597 [1].

Exploitation

The attacker must be an authenticated team member in the Mattermost server and have the Manage Own Slash Commands permission. They can craft an API request to update an existing slash command they own, changing its trigger word to an already-taken trigger (e.g., one used by a system slash command or another custom command). The server fails to validate uniqueness at update time, allowing the conflicting trigger to be saved.

Impact

Upon successful exploitation, the attacker's slash command replaces the original command for the team. When any user types the hijacked trigger, the attacker-defined command executes instead of the intended system or custom command. This can lead to impersonation, unauthorized data access, or execution of arbitrary actions within the context of the attacker's command handler, depending on the command's capabilities.

Mitigation

As of the publication date (2026-05-18), no fix has been disclosed in the available references. Affected users should monitor the Mattermost security updates page [1] for patches. Workarounds may include restricting the Manage Own Slash Commands permission to trusted users or auditing custom slash command triggers manually.

AI Insight generated on May 21, 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.0, <= 11.5.1 OR >= 10.11.0, <= 10.11.13 OR >= 11.4.0, <= 11.4.3

Patches

1
2e4956a92725

MM-67646 slack import improvements (#35490) (#35648)

https://github.com/mattermost/mattermostMattermost BuildMar 17, 2026Fixed in 11.4.4via llm-release-walk
4 files changed · +142 9
  • server/channels/app/slack.go+8 0 modified
    @@ -39,6 +39,14 @@ func (a *App) SlackImport(rctx request.CTX, fileData multipart.File, fileSize in
     		GeneratePreviewImage:   a.generatePreviewImage,
     		InvalidateAllCaches:    func() *model.AppError { return a.ch.srv.platform.InvalidateAllCaches() },
     		MaxPostSize:            func() int { return a.ch.srv.platform.MaxPostSize() },
    +		SendPasswordReset: func(email string) (bool, *model.AppError) {
    +			sent, err := a.SendPasswordReset(rctx, email, a.GetSiteURL())
    +			if err != nil {
    +				return false, err
    +			}
    +
    +			return sent, nil
    +		},
     		PrepareImage: func(fileData []byte) (image.Image, string, func(), error) {
     			img, imgType, release, err := prepareImage(rctx, a.ch.imgDecoder, bytes.NewReader(fileData))
     			if err != nil {
    
  • server/i18n/en.json+8 4 modified
    @@ -3207,8 +3207,8 @@
         "translation": "Could not uninvite remote to channel"
       },
       {
    -    "id": "api.slackimport.slack_add_bot_user.email_pwd",
    -    "translation": "The Integration/Slack Bot user with email {{.Email}} and password {{.Password}} has been imported.\r\n"
    +    "id": "api.slackimport.slack_add_bot_user.email",
    +    "translation": "The Integration/Slack Bot user with email {{.Email}} has been imported.\r\n"
       },
       {
         "id": "api.slackimport.slack_add_bot_user.unable_import",
    @@ -3235,8 +3235,8 @@
         "translation": "\r\nUsers created:\r\n"
       },
       {
    -    "id": "api.slackimport.slack_add_users.email_pwd",
    -    "translation": "Slack user with email {{.Email}} and password {{.Password}} has been imported.\r\n"
    +    "id": "api.slackimport.slack_add_users.email",
    +    "translation": "Slack user with email {{.Email}} has been imported.\r\n"
       },
       {
         "id": "api.slackimport.slack_add_users.merge_existing",
    @@ -3250,6 +3250,10 @@
         "id": "api.slackimport.slack_add_users.missing_email_address",
         "translation": "User {{.Username}} does not have an email address in the Slack export. Used {{.Email}} as a placeholder. The user should update their email address once logged in to the system.\r\n"
       },
    +  {
    +    "id": "api.slackimport.slack_add_users.send_reset_email_failed",
    +    "translation": "Unable to send password reset email to {{.Username}} at {{.Email}}.\r\n"
    +  },
       {
         "id": "api.slackimport.slack_add_users.unable_import",
         "translation": "Unable to import Slack user: {{.Username}}.\r\n"
    
  • server/platform/services/slackimport/slackimport.go+14 5 modified
    @@ -95,6 +95,7 @@ type Actions struct {
     	GeneratePreviewImage   func(request.CTX, image.Image, string, string)
     	InvalidateAllCaches    func() *model.AppError
     	MaxPostSize            func() int
    +	SendPasswordReset      func(string) (bool, *model.AppError)
     	PrepareImage           func(fileData []byte) (image.Image, string, func(), error)
     }
     
    @@ -268,8 +269,6 @@ func (si *SlackImporter) slackAddUsers(rctx request.CTX, teamId string, slackuse
     			rctx.Logger().Warn("Slack Import: User does not have an email address in the Slack export. Used username as a placeholder. The user should update their email address once logged in to the system.", mlog.String("user_email", email), mlog.String("user_name", sUser.Username))
     		}
     
    -		password := model.NewId()
    -
     		// Check for email conflict and use existing user if found
     		if existingUser, err := si.store.User().GetByEmail(email); err == nil {
     			addedUsers[sUser.Id] = existingUser
    @@ -287,16 +286,26 @@ func (si *SlackImporter) slackAddUsers(rctx request.CTX, teamId string, slackuse
     			FirstName: firstName,
     			LastName:  lastName,
     			Email:     email,
    -			Password:  password,
    +			Password:  "",
     		}
     
     		mUser := si.oldImportUser(rctx, team, &newUser)
     		if mUser == nil {
     			importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.unable_import", map[string]any{"Username": sUser.Username}))
     			continue
     		}
    +
    +		sent, err := si.actions.SendPasswordReset(email)
    +		if err != nil {
    +			rctx.Logger().Warn("Slack Import: Cannot send password reset email to user. An admin should update their email address once logged in to the system.", mlog.String("user_email", email), mlog.String("user_name", sUser.Username))
    +		}
    +
    +		if !sent {
    +			importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.send_reset_email_failed", map[string]any{"Username": sUser.Username, "Email": newUser.Email}))
    +		}
    +
     		addedUsers[sUser.Id] = mUser
    -		importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.email_pwd", map[string]any{"Email": newUser.Email, "Password": password}))
    +		importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.email", map[string]any{"Email": newUser.Email}))
     	}
     
     	return addedUsers
    @@ -327,7 +336,7 @@ func (si *SlackImporter) slackAddBotUser(rctx request.CTX, teamId string, log *b
     		return nil
     	}
     
    -	log.WriteString(i18n.T("api.slackimport.slack_add_bot_user.email_pwd", map[string]any{"Email": botUser.Email, "Password": password}))
    +	log.WriteString(i18n.T("api.slackimport.slack_add_bot_user.email", map[string]any{"Email": botUser.Email}))
     	return mUser
     }
     
    
  • server/platform/services/slackimport/slackimport_test.go+112 0 modified
    @@ -6,6 +6,7 @@ package slackimport
     import (
     	"archive/zip"
     	"bytes"
    +	"fmt"
     	"os"
     	"path/filepath"
     	"strings"
    @@ -760,3 +761,114 @@ func TestSlackImportEnhancedSecurityBackwardsCompatibility(t *testing.T) {
     	// Verify VerifyEmail was NOT called
     	userStore.AssertNotCalled(t, "VerifyEmail")
     }
    +
    +type slackAddUsersTestSetup struct {
    +	store     *mocks.Store
    +	teamStore *mocks.TeamStore
    +	userStore *mocks.UserStore
    +	team      *model.Team
    +	savedUser *model.User
    +}
    +
    +func newSlackAddUsersTestSetup(t *testing.T) *slackAddUsersTestSetup {
    +	t.Helper()
    +
    +	s := &slackAddUsersTestSetup{}
    +	s.store = &mocks.Store{}
    +	s.teamStore = &mocks.TeamStore{}
    +	s.userStore = &mocks.UserStore{}
    +	s.store.On("Team").Return(s.teamStore)
    +	s.store.On("User").Return(s.userStore)
    +
    +	s.team = &model.Team{Id: "test-team-id", Name: "test-team"}
    +	s.teamStore.On("Get", "test-team-id").Return(s.team, nil)
    +	s.userStore.On("GetByEmail", mock.AnythingOfType("string")).Return(nil, fmt.Errorf("not found"))
    +
    +	s.savedUser = &model.User{Id: "test-user-id", Username: "testuser", Email: "testuser@example.com"}
    +	s.userStore.On("Save", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.User")).Return(s.savedUser, nil)
    +
    +	return s
    +}
    +
    +func (s *slackAddUsersTestSetup) newImporter(actions Actions) *SlackImporter {
    +	config := &model.Config{}
    +	config.SetDefaults()
    +	return New(s.store, actions, config)
    +}
    +
    +func (s *slackAddUsersTestSetup) defaultActions() Actions {
    +	return Actions{
    +		JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) {
    +			return &model.TeamMember{}, nil
    +		},
    +		SendPasswordReset: func(email string) (bool, *model.AppError) {
    +			return true, nil
    +		},
    +	}
    +}
    +
    +func defaultSlackUsers() []slackUser {
    +	return []slackUser{
    +		{Id: "U001", Username: "testuser", Profile: slackProfile{FirstName: "Test", LastName: "User", Email: "testuser@example.com"}},
    +	}
    +}
    +
    +func TestSlackAddUsersLogContainsProperUserCreationMessage(t *testing.T) {
    +	rctx := request.TestContext(t)
    +	s := newSlackAddUsersTestSetup(t)
    +
    +	importer := s.newImporter(s.defaultActions())
    +	importerLog := new(bytes.Buffer)
    +	importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog)
    +
    +	logOutput := importerLog.String()
    +	assert.Contains(t, logOutput, "api.slackimport.slack_add_users.email", "import log should contain the user creation message")
    +	assert.NotContains(t, logOutput, "api.slackimport.slack_add_users.email_pwd", "import log must not use the old user creation message")
    +}
    +
    +func TestSlackAddUsersLogsSendResetEmailFailure(t *testing.T) {
    +	rctx := request.TestContext(t)
    +	s := newSlackAddUsersTestSetup(t)
    +
    +	actions := s.defaultActions()
    +	actions.SendPasswordReset = func(email string) (bool, *model.AppError) {
    +		return false, nil
    +	}
    +
    +	importer := s.newImporter(actions)
    +	importerLog := new(bytes.Buffer)
    +	importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog)
    +
    +	assert.Contains(t, importerLog.String(), "api.slackimport.slack_add_users.send_reset_email_failed")
    +}
    +
    +func TestSlackAddUsersGeneratesUserWithEmptyPassword(t *testing.T) {
    +	rctx := request.TestContext(t)
    +	s := newSlackAddUsersTestSetup(t)
    +
    +	importer := s.newImporter(s.defaultActions())
    +	importerLog := new(bytes.Buffer)
    +	importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog)
    +
    +	s.userStore.AssertCalled(t, "Save", mock.AnythingOfType("*request.Context"), mock.MatchedBy(func(u *model.User) bool {
    +		return u.Password == ""
    +	}))
    +}
    +
    +func TestSlackAddUsersTriggersPasswordResetFlow(t *testing.T) {
    +	rctx := request.TestContext(t)
    +	s := newSlackAddUsersTestSetup(t)
    +
    +	passwordResetCalled := false
    +	actions := s.defaultActions()
    +	actions.SendPasswordReset = func(email string) (bool, *model.AppError) {
    +		passwordResetCalled = true
    +		return true, nil
    +	}
    +
    +	importer := s.newImporter(actions)
    +	importerLog := new(bytes.Buffer)
    +	importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog)
    +
    +	assert.True(t, passwordResetCalled, "SendPasswordReset should be called for each imported user")
    +}
    

Vulnerability mechanics

Root cause

"Missing uniqueness check on slash command trigger-word during command updates allows an authenticated user to overwrite an existing system or custom slash command's trigger."

Attack vector

An authenticated team member with "Manage Own Slash Commands" permission sends a PUT request to the command update API, changing the trigger word of their own custom slash command to match a trigger word already registered by a system command or another user's command. The server fails to validate that the new trigger word is unique across all commands for that team [CWE-863]. The attacker does not need special privileges beyond standard team membership and the ability to manage their own slash commands. The payload is simply a JSON body containing the desired duplicate trigger word in the "trigger" field.

Affected code

The advisory does not specify the exact file paths or functions at fault. The vulnerability exists in the command update API endpoint that handles PUT requests for modifying slash commands. The patch shown (patch_id=918532) is unrelated to this CVE—it modifies Slack import logic in server/platform/services/slackimport/slackimport.go and server/channels/app/slack.go to remove password logging and add password-reset flows.

What the fix does

The advisory states that Mattermost versions 11.5.x <= 11.5.1, 10.11.x <= 10.11.13, and 11.4.x <= 11.4.3 fail to enforce slash command trigger-word uniqueness during command updates. The patch (patch_id=918532) shown in the bundle addresses an unrelated Slack import password-handling change and does not contain the actual fix for this vulnerability. Based on the advisory description, the fix would add a uniqueness check in the command update API handler to verify that the proposed trigger word does not already belong to another active command before allowing the update.

Preconditions

  • authAttacker must be authenticated to the Mattermost server
  • authAttacker must be a team member with 'Manage Own Slash Commands' permission
  • inputAttacker must have a custom slash command they can edit via the command update API
  • configA target slash command (system or custom) with a trigger word must already exist on the same team

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.