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

CVE-2026-2325

CVE-2026-2325

Description

Mattermost versions 11.5.x <= 11.5.1, 10.11.x <= 10.11.13, 11.4.x <= 11.4.3 fail to limit the size of the request body on the start meeting API endpoint, which allows an authenticated attacker to cause resource exhaustion or denial of service via a crafted oversized HTTP POST request to {{/api/v1/meetings}}.. Mattermost Advisory ID: MMSA-2026-00608

AI Insight

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

Mattermost fails to limit request body size on the start meeting API, allowing authenticated attackers to cause resource exhaustion or denial of service.

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 a maximum request body size on the start meeting API endpoint (/api/v1/meetings). This allows an authenticated attacker to send a crafted oversized HTTP POST request, leading to resource exhaustion or denial of service [1].

Exploitation

An attacker must be authenticated to Mattermost and able to reach the vulnerable endpoint. The attack involves sending a single oversized HTTP POST request to the start meeting API with an excessively large body. No special privileges or user interaction beyond standard authentication are required. The crafted request consumes server resources (memory/CPU) beyond expected limits [1].

Impact

Successful exploitation results in resource exhaustion, leading to a denial of service condition for the affected Mattermost server. The confidentiality and integrity of data are not compromised, but availability is degraded or completely interrupted [1].

Mitigation

Mattermost has addressed this vulnerability in versions 11.5.2, 10.11.14, and 11.4.4. Organizations should upgrade to these patched versions. No workarounds are documented in the reference. This CVE is not listed on CISA's Known Exploited Vulnerabilities (KEV) catalog [1].

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

Patches

1
574401e6c3e3

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

https://github.com/mattermost/mattermostMattermost BuildMar 17, 2026Fixed in 11.4.4via 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
    @@ -687,22 +687,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)
    @@ -719,6 +705,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)
    @@ -752,6 +762,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
    @@ -770,6 +784,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
    @@ -1326,6 +1326,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
    @@ -5370,6 +5370,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

"The start meeting API endpoint does not limit the size of the request body, allowing an authenticated attacker to send an oversized HTTP POST request and exhaust server resources."

Attack vector

An authenticated attacker sends a crafted oversized HTTP POST request to the `/api/v1/meetings` endpoint. Because the endpoint fails to enforce any limit on the request body size [CWE-770], the server allocates resources proportional to the payload without throttling. A sufficiently large payload can exhaust memory or CPU, leading to denial of service for other users. The attack requires only low-privilege authentication and is performed over the network.

Affected code

The advisory identifies the start meeting API endpoint at `/api/v1/meetings` as the vulnerable code path. The patch bundle does not include the specific server-side file or function where the request body size limit was added. The patches provided instead modify `server/channels/app/command.go`, `server/channels/api4/command_test.go`, `server/public/model/command.go`, and related test files to improve command trigger validation.

What the fix does

The advisory states that the fix limits the request body size on the start meeting API endpoint, but the supplied patch bundle does not contain the actual server-side fix for the `/api/v1/meetings` endpoint. The patches shown instead refactor command trigger uniqueness validation into a shared helper (`validateCommandTriggerUniqueness`) and add stricter trigger character validation via a regex in `command.go`. These changes are unrelated to the body-size limit issue described in the CVE. The advisory does not include the specific diff that adds the request body size limit.

Preconditions

  • authThe attacker must be authenticated to the Mattermost instance.
  • networkThe attacker must be able to send HTTP POST requests to the /api/v1/meetings endpoint.
  • configNo restriction on request body size is enforced by the server on that endpoint.

Generated on May 21, 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.