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
12e4956a92725MM-67646 slack import improvements (#35490) (#35648)
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- mattermost.com/security-updatesnvdVendor Advisory
News mentions
0No linked articles in our index yet.