VYPR
Moderate severityNVD Advisory· Published Mar 16, 2026· Updated Mar 16, 2026

WebSocket Message Spoofing via Permalink Embed Manipulation

CVE-2026-2457

Description

Mattermost versions 11.3.x <= 11.3.0, 11.2.x <= 11.2.2, 10.11.x <= 10.11.10 fail to sanitize client-supplied post metadata which allows an authenticated attacker to spoof permalink embeds impersonating other users via crafted PUT requests to the post update API endpoint.. Mattermost Advisory ID: MMSA-2025-00569

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost/server/v8Go
< 8.0.0-20260123211116-9efe617be8b88.0.0-20260123211116-9efe617be8b8
github.com/mattermost/mattermost-serverGo
< 5.3.2-0.20260123211116-9efe617be8b85.3.2-0.20260123211116-9efe617be8b8
github.com/mattermost/mattermost-serverGo
>= 10.11.0-rc1, < 10.11.1110.11.11
github.com/mattermost/mattermost-serverGo
>= 11.2.0-rc1, < 11.2.311.2.3
github.com/mattermost/mattermost-serverGo
>= 11.3.0-rc1, < 11.3.111.3.1

Affected products

1

Patches

1
9efe617be8b8

MM-67055: Fix permalink embeds in WebSocket messages (#34893)

https://github.com/mattermost/mattermostChristopher PoileJan 23, 2026via ghsa
4 files changed · +107 0
  • server/channels/api4/post.go+4 0 modified
    @@ -1010,6 +1010,10 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    +	// MM-67055: Strip client-supplied metadata.embeds to prevent spoofing.
    +	// This matches createPost behavior.
    +	post.SanitizeInput()
    +
     	auditRec := c.MakeAuditRecord(model.AuditEventUpdatePost, model.AuditStatusFail)
     	model.AddEventParameterAuditableToAuditRec(auditRec, "post", &post)
     	defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
    
  • server/channels/api4/post_test.go+47 0 modified
    @@ -1552,6 +1552,53 @@ func TestUpdatePost(t *testing.T) {
     		assert.NotEqual(t, rpost3.Attachments(), rrupost3.Attachments())
     	})
     
    +	t.Run("should strip spoofed metadata embeds", func(t *testing.T) {
    +		// MM-67055: Verify that client-supplied metadata.embeds are stripped
    +		post := &model.Post{
    +			ChannelId: channel.Id,
    +			Message:   "test message " + model.NewId(),
    +		}
    +		createdPost, _, err := client.CreatePost(context.Background(), post)
    +		require.NoError(t, err)
    +
    +		// Try to update with spoofed embed
    +		updatePost := &model.Post{
    +			Id:        createdPost.Id,
    +			ChannelId: channel.Id,
    +			Message:   "updated message " + model.NewId(),
    +			Metadata: &model.PostMetadata{
    +				Embeds: []*model.PostEmbed{
    +					{
    +						Type: model.PostEmbedPermalink,
    +						Data: &model.PreviewPost{
    +							PostID: "spoofed-post-id",
    +							Post: &model.Post{
    +								Id:      "spoofed-post-id",
    +								UserId:  th.BasicUser2.Id,
    +								Message: "This is a spoofed message!",
    +							},
    +						},
    +					},
    +				},
    +			},
    +		}
    +
    +		updatedPost, _, err := client.UpdatePost(context.Background(), createdPost.Id, updatePost)
    +		require.NoError(t, err)
    +
    +		// Verify spoofed embed was stripped
    +		if updatedPost.Metadata != nil {
    +			assert.Empty(t, updatedPost.Metadata.Embeds, "spoofed embeds should be stripped")
    +		}
    +
    +		// Double-check by fetching the post
    +		fetchedPost, _, err := client.GetPost(context.Background(), createdPost.Id, "")
    +		require.NoError(t, err)
    +		if fetchedPost.Metadata != nil {
    +			assert.Empty(t, fetchedPost.Metadata.Embeds, "spoofed embeds should not be persisted")
    +		}
    +	})
    +
     	t.Run("change message, but post too old", func(t *testing.T) {
     		th.App.UpdateConfig(func(cfg *model.Config) {
     			*cfg.ServiceSettings.PostEditTimeLimit = 1
    
  • server/channels/app/post.go+2 0 modified
    @@ -849,6 +849,8 @@ func (a *App) UpdatePost(rctx request.CTX, receivedUpdatedPost *model.Post, upda
     	// Always use incoming metadata when provided, otherwise retain existing
     	if receivedUpdatedPost.Metadata != nil {
     		newPost.Metadata = receivedUpdatedPost.Metadata.Copy()
    +		// MM-67055: Strip embeds - always server-generated. Preserves Priority/Acks for Shared Channels sync.
    +		newPost.Metadata.Embeds = nil
     	} else {
     		// Restore the post metadata that was stripped by the plugin. Set it to
     		// the last known good.
    
  • server/channels/app/post_test.go+54 0 modified
    @@ -1952,6 +1952,60 @@ func TestUpdatePost(t *testing.T) {
     		}
     	})
     
    +	t.Run("should strip client-supplied embeds", func(t *testing.T) {
    +		// MM-67055: Verify that client-supplied metadata.embeds are stripped.
    +		// This prevents WebSocket message spoofing via permalink embeds.
    +		//
    +		// Note: Priority and Acknowledgements are stored in separate database tables,
    +		// not in post metadata. Shared Channels handles them separately via
    +		// syncRemotePriorityMetadata and syncRemoteAcknowledgementsMetadata after
    +		// calling UpdatePost. See sync_recv.go::upsertSyncPost
    +		mainHelper.Parallel(t)
    +		th := Setup(t).InitBasic(t)
    +
    +		th.AddUserToChannel(t, th.BasicUser, th.BasicChannel)
    +		th.Context.Session().UserId = th.BasicUser.Id
    +
    +		// Create a basic post
    +		post := &model.Post{
    +			ChannelId: th.BasicChannel.Id,
    +			Message:   "original message",
    +			UserId:    th.BasicUser.Id,
    +		}
    +		createdPost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
    +		require.Nil(t, err)
    +
    +		// Try to update with spoofed embeds (the attack vector)
    +		updatePost := &model.Post{
    +			Id:        createdPost.Id,
    +			ChannelId: th.BasicChannel.Id,
    +			Message:   "updated message",
    +			UserId:    th.BasicUser.Id,
    +			Metadata: &model.PostMetadata{
    +				Embeds: []*model.PostEmbed{
    +					{
    +						Type: model.PostEmbedPermalink,
    +						Data: &model.PreviewPost{
    +							PostID: "spoofed-post-id",
    +							Post: &model.Post{
    +								Id:      "spoofed-post-id",
    +								UserId:  th.BasicUser2.Id,
    +								Message: "Spoofed message from another user!",
    +							},
    +						},
    +					},
    +				},
    +			},
    +		}
    +
    +		updatedPost, err := th.App.UpdatePost(th.Context, updatePost, nil)
    +		require.Nil(t, err)
    +		require.NotNil(t, updatedPost.Metadata)
    +
    +		// Verify embeds were stripped
    +		assert.Empty(t, updatedPost.Metadata.Embeds, "spoofed embeds should be stripped")
    +	})
    +
     	t.Run("cannot update post in restricted DM", func(t *testing.T) {
     		mainHelper.Parallel(t)
     		th := Setup(t).InitBasic(t)
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.