VYPR
Low severity3.5NVD Advisory· Published May 18, 2026· Updated May 19, 2026

CVE-2026-6333

CVE-2026-6333

Description

Mattermost versions 11.5.x <= 11.5.1, 10.11.x <= 10.11.13 fail to validate the Host header when constructing response URLs for custom slash commands which allows an authenticated attacker to redirect slash command responses to an attacker-controlled server via a spoofed Host header.. Mattermost Advisory ID: MMSA-2026-00582

AI Insight

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

Mattermost fails to validate the Host header in custom slash command URLs, allowing authenticated attackers to redirect responses to an attacker-controlled server.

Vulnerability

Mattermost versions 11.5.x up to 11.5.1 and 10.11.x up to 10.11.13 do not validate the Host header when constructing response URLs for custom slash commands. This allows an authenticated attacker to exploit a host header injection to redirect slash command responses to an attacker-controlled server. [1]

Exploitation

An authenticated attacker can send a request with a spoofed Host header when interacting with a custom slash command. The Mattermost server uses the Host header to construct response URLs, thereby redirecting the command response to the attacker's server. No user interaction is required beyond the attacker being authenticated.

Impact

The attacker can redirect slash command responses to an external server, potentially leading to information disclosure or phishing attacks. The vulnerability has a CVSS score of 3.5 (Low) and is rated as low severity.

Mitigation

As of the publication date, no fixed version has been announced. Users are advised to monitor Mattermost security updates for patches. [1] If a fix is released, upgrading to the latest version is recommended.

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
3d3bf0dfd049

[MM-67143] Fix for custom slash command response URL (#34922) (#35774)

https://github.com/mattermost/mattermostMattermost BuildMar 26, 2026Fixed in 11.5.2via llm-release-walk
4 files changed · +168 2
  • server/channels/api4/command_test.go+141 0 modified
    @@ -6,6 +6,7 @@ package api4
     import (
     	"context"
     	"encoding/json"
    +	"fmt"
     	"net/http"
     	"net/http/httptest"
     	"net/url"
    @@ -17,6 +18,7 @@ import (
     
     	"github.com/mattermost/mattermost/server/public/model"
     	"github.com/mattermost/mattermost/server/public/shared/mlog"
    +	"github.com/mattermost/mattermost/server/v8/channels/testlib"
     )
     
     func TestCreateCommand(t *testing.T) {
    @@ -1389,14 +1391,17 @@ func TestExecuteInvalidCommand(t *testing.T) {
     
     	enableCommands := *th.App.Config().ServiceSettings.EnableCommands
     	allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
    +	siteURL := *th.App.Config().ServiceSettings.SiteURL
     	defer func() {
     		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
     		th.App.UpdateConfig(func(cfg *model.Config) {
     			cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
     		})
    +		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = &siteURL })
     	}()
     	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
     	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.0/8" })
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
     
     	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
     		rc := &model.CommandResponse{}
    @@ -1461,14 +1466,17 @@ func TestExecuteGetCommand(t *testing.T) {
     
     	enableCommands := *th.App.Config().ServiceSettings.EnableCommands
     	allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
    +	siteURL := *th.App.Config().ServiceSettings.SiteURL
     	defer func() {
     		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
     		th.App.UpdateConfig(func(cfg *model.Config) {
     			cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
     		})
    +		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = &siteURL })
     	}()
     	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
     	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.0/8" })
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
     
     	token := model.NewId()
     	expectedCommandResponse := &model.CommandResponse{
    @@ -1523,14 +1531,17 @@ func TestExecutePostCommand(t *testing.T) {
     
     	enableCommands := *th.App.Config().ServiceSettings.EnableCommands
     	allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
    +	siteURL := *th.App.Config().ServiceSettings.SiteURL
     	defer func() {
     		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
     		th.App.UpdateConfig(func(cfg *model.Config) {
     			cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
     		})
    +		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = &siteURL })
     	}()
     	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
     	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.0/8" })
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
     
     	token := model.NewId()
     	expectedCommandResponse := &model.CommandResponse{
    @@ -1692,16 +1703,19 @@ func TestExecuteCommandInDirectMessageChannel(t *testing.T) {
     
     	enableCommands := *th.App.Config().ServiceSettings.EnableCommands
     	allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
    +	siteURL := *th.App.Config().ServiceSettings.SiteURL
     	defer func() {
     		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
     		th.App.UpdateConfig(func(cfg *model.Config) {
     			cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
     		})
    +		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = &siteURL })
     	}()
     	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
     	th.App.UpdateConfig(func(cfg *model.Config) {
     		*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
     	})
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
     
     	// create a team that the user isn't a part of
     	team2 := th.CreateTeam(t)
    @@ -1756,16 +1770,19 @@ func TestExecuteCommandInTeamUserIsNotOn(t *testing.T) {
     
     	enableCommands := *th.App.Config().ServiceSettings.EnableCommands
     	allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
    +	siteURL := *th.App.Config().ServiceSettings.SiteURL
     	defer func() {
     		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
     		th.App.UpdateConfig(func(cfg *model.Config) {
     			cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
     		})
    +		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = &siteURL })
     	}()
     	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
     	th.App.UpdateConfig(func(cfg *model.Config) {
     		*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
     	})
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
     
     	// create a team that the user isn't a part of
     	team2 := th.CreateTeam(t)
    @@ -1833,16 +1850,19 @@ func TestExecuteCommandReadOnly(t *testing.T) {
     
     	enableCommands := *th.App.Config().ServiceSettings.EnableCommands
     	allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
    +	siteURL := *th.App.Config().ServiceSettings.SiteURL
     	defer func() {
     		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
     		th.App.UpdateConfig(func(cfg *model.Config) {
     			cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
     		})
    +		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = &siteURL })
     	}()
     	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
     	th.App.UpdateConfig(func(cfg *model.Config) {
     		*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
     	})
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
     
     	expectedCommandResponse := &model.CommandResponse{
     		Text:         "test post command response",
    @@ -1920,3 +1940,124 @@ func TestExecuteCommandReadOnly(t *testing.T) {
     	require.Error(t, err)
     	CheckBadRequestStatus(t, resp)
     }
    +
    +func TestExecuteCommandResponseURLUsesSiteURL(t *testing.T) {
    +	mainHelper.Parallel(t)
    +	th := Setup(t).InitBasic(t)
    +	client := th.Client
    +	channel := th.BasicChannel
    +
    +	enableCommands := *th.App.Config().ServiceSettings.EnableCommands
    +	allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
    +	siteURL := *th.App.Config().ServiceSettings.SiteURL
    +	defer func() {
    +		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
    +		th.App.UpdateConfig(func(cfg *model.Config) {
    +			cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
    +		})
    +		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = &siteURL })
    +	}()
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.0/8" })
    +
    +	// Set a SiteURL that differs from the test client's Host header (localhost).
    +	// This verifies that response_url uses the configured SiteURL, not the Host header.
    +	// Before the fix (MM-67142), response_url would contain "localhost" and fail this check.
    +	expectedSiteURL := "http://mattermost.example.com"
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = expectedSiteURL })
    +
    +	var receivedResponseURL string
    +	expectedCommandResponse := &model.CommandResponse{
    +		Text:         "test response_url command response",
    +		ResponseType: model.CommandResponseTypeInChannel,
    +	}
    +
    +	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		err := r.ParseForm()
    +		require.NoError(t, err)
    +
    +		// Capture the response_url sent by the server
    +		receivedResponseURL = r.FormValue("response_url")
    +
    +		w.Header().Set("Content-Type", "application/json")
    +		if err := json.NewEncoder(w).Encode(expectedCommandResponse); err != nil {
    +			th.TestLogger.Warn("Error while writing response", mlog.Err(err))
    +		}
    +	}))
    +	defer ts.Close()
    +
    +	postCmd := &model.Command{
    +		CreatorId: th.BasicUser.Id,
    +		TeamId:    th.BasicTeam.Id,
    +		URL:       ts.URL,
    +		Method:    model.CommandMethodPost,
    +		Trigger:   "testrespurl",
    +	}
    +
    +	_, appErr := th.App.CreateCommand(postCmd)
    +	require.Nil(t, appErr, "failed to create command")
    +
    +	_, _, err := client.ExecuteCommand(context.Background(), channel.Id, "/testrespurl")
    +	require.NoError(t, err)
    +
    +	// Verify response_url starts with the configured SiteURL, not the Host header
    +	require.True(t, strings.HasPrefix(receivedResponseURL, expectedSiteURL),
    +		"response_url should start with configured SiteURL %q, but got %q", expectedSiteURL, receivedResponseURL)
    +
    +	// Verify warning is logged when Host header differs from SiteURL
    +	require.NoError(t, th.TestLogger.Flush())
    +	testlib.AssertLog(t, th.LogBuffer, mlog.LvlWarn.Name, "Request hostname differs from configured SiteURL. The configured SiteURL will be used for the slash command response URL.")
    +}
    +
    +func TestExecuteCustomCommandRequiresSiteURL(t *testing.T) {
    +	mainHelper.Parallel(t)
    +	th := Setup(t).InitBasic(t)
    +	client := th.Client
    +	channel := th.BasicChannel
    +
    +	enableCommands := *th.App.Config().ServiceSettings.EnableCommands
    +	allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections
    +	siteURL := *th.App.Config().ServiceSettings.SiteURL
    +	defer func() {
    +		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands })
    +		th.App.UpdateConfig(func(cfg *model.Config) {
    +			cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections
    +		})
    +		th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = &siteURL })
    +	}()
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true })
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.0/8" })
    +
    +	// Set SiteURL to a valid value first so we can create the command
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "http://localhost:8065" })
    +
    +	// Create a custom command
    +	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		w.Header().Set("Content-Type", "application/json")
    +		fmt.Fprintln(w, `{"text": "response"}`)
    +	}))
    +	defer ts.Close()
    +
    +	postCmd := &model.Command{
    +		CreatorId: th.BasicUser.Id,
    +		TeamId:    th.BasicTeam.Id,
    +		URL:       ts.URL,
    +		Method:    model.CommandMethodPost,
    +		Trigger:   "customcmd",
    +	}
    +	_, appErr := th.App.CreateCommand(postCmd)
    +	require.Nil(t, appErr, "failed to create command")
    +
    +	// Now set SiteURL to empty
    +	th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SiteURL = "" })
    +
    +	// Try to execute the custom command - should fail because SiteURL is required for custom commands
    +	_, resp, err := client.ExecuteCommand(context.Background(), channel.Id, "/customcmd")
    +	require.Error(t, err)
    +	CheckBadRequestStatus(t, resp)
    +	CheckErrorID(t, err, "api.command.execute_command.site_url_required.app_error")
    +
    +	// Built-in commands should still work without SiteURL
    +	_, _, err = client.ExecuteCommand(context.Background(), channel.Id, "/echo test")
    +	require.NoError(t, err)
    +}
    
  • server/channels/app/command_autocomplete.go+6 1 modified
    @@ -269,7 +269,12 @@ func (a *App) getDynamicListArgument(rctx request.CTX, commandArgs *model.Comman
     	params.Add("team_id", commandArgs.TeamId)
     	params.Add("root_id", commandArgs.RootId)
     	params.Add("user_id", commandArgs.UserId)
    -	params.Add("site_url", commandArgs.SiteURL)
    +
    +	// Use configured SiteURL to prevent SSRF via Host header spoofing (MM-67142)
    +	siteURL := *a.Config().ServiceSettings.SiteURL
    +	if siteURL != "" {
    +		params.Add("site_url", siteURL)
    +	}
     
     	resp, err := a.doPluginRequest(rctx, "GET", dynamicArg.FetchURL, params, nil)
     
    
  • server/channels/app/command.go+13 1 modified
    @@ -497,11 +497,23 @@ func (a *App) tryExecuteCustomCommand(rctx request.CTX, args *model.CommandArgs,
     	channelMentionMap := a.MentionsToPublicChannels(rctx, message, team.Id)
     	maps.Copy(p, channelMentionMap.ToURLValues())
     
    +	// Use configured SiteURL for response_url to prevent SSRF via Host header spoofing (MM-67142)
    +	siteURL := *a.Config().ServiceSettings.SiteURL
    +	if siteURL == "" {
    +		return cmd, nil, model.NewAppError("tryExecuteCustomCommand", "api.command.execute_command.site_url_required.app_error", nil, "", http.StatusBadRequest)
    +	}
    +	if siteURL != args.SiteURL {
    +		rctx.Logger().Warn(i18n.T("api.command.execute_command.provided_site_url_different.app_error"),
    +			mlog.String("request_host", args.SiteURL),
    +			mlog.String("configured_site_url", siteURL),
    +			mlog.String("command", trigger))
    +	}
    +
     	hook, appErr := a.CreateCommandWebhook(cmd.Id, args)
     	if appErr != nil {
     		return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": trigger}, "", http.StatusInternalServerError).Wrap(appErr)
     	}
    -	p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
    +	p.Set("response_url", siteURL+"/hooks/commands/"+hook.Id)
     
     	return a.DoCommandRequest(rctx, cmd, p)
     }
    
  • server/i18n/en.json+8 0 modified
    @@ -723,10 +723,18 @@
         "id": "api.command.execute_command.not_found.app_error",
         "translation": "Command with a trigger of '{{.Trigger}}' not found. To send a message beginning with \"/\", try adding an empty space at the beginning of the message."
       },
    +  {
    +    "id": "api.command.execute_command.provided_site_url_different.app_error",
    +    "translation": "Request hostname differs from configured SiteURL. The configured SiteURL will be used for the slash command response URL."
    +  },
       {
         "id": "api.command.execute_command.restricted_dm.error",
         "translation": "Cannot post in a restricted DM"
       },
    +  {
    +    "id": "api.command.execute_command.site_url_required.app_error",
    +    "translation": "SiteURL must be configured to use slash commands"
    +  },
       {
         "id": "api.command.execute_command.start.app_error",
         "translation": "No command trigger found."
    

Vulnerability mechanics

Root cause

"The server constructs the slash command response URL using the Host header from the incoming HTTP request instead of the configured SiteURL, allowing an attacker to redirect the response to an arbitrary server."

Attack vector

An authenticated attacker sends a slash command request with a spoofed Host header pointing to an attacker-controlled server. The vulnerable code in `tryExecuteCustomCommand` [patch_id=918519] uses `args.SiteURL` (derived from the Host header) to build the `response_url` parameter sent to the custom command's external endpoint. The external command server receives this `response_url` and can POST the command response to the attacker's server instead of the legitimate Mattermost instance. The attacker must be authenticated and custom slash commands must be enabled.

Affected code

The vulnerability exists in `server/channels/app/command.go` in the `tryExecuteCustomCommand` function, where `args.SiteURL` (derived from the HTTP Host header) was used to build the `response_url`. The same pattern appears in `server/channels/app/command_autocomplete.go` for the `site_url` parameter in dynamic list arguments. The patch modifies both files to use the configured `ServiceSettings.SiteURL` instead.

What the fix does

The patch changes `tryExecuteCustomCommand` [patch_id=918519] to use the configured `SiteURL` from `a.Config().ServiceSettings.SiteURL` instead of `args.SiteURL` (which originates from the Host header) when constructing the `response_url`. It also adds a validation that returns a `BadRequest` error if `SiteURL` is empty, preventing execution of custom commands without a configured SiteURL. Additionally, a warning is logged when the request hostname differs from the configured SiteURL. The same fix is applied in `command_autocomplete.go` for the `site_url` parameter sent to dynamic list arguments.

Preconditions

  • authAttacker must be authenticated to the Mattermost instance
  • configCustom slash commands must be enabled (ServiceSettings.EnableCommands = true)
  • inputAttacker must be able to spoof the Host header in the HTTP request to the Mattermost server

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.