VYPR
Unrated severityNVD Advisory· Published May 22, 2026· Updated May 22, 2026

Unauthenticated WebSocket binary frame causes denial of service in Mattermost Server

CVE-2026-5740

Description

Mattermost versions 11.6.x <= 11.6.0, 11.5.x <= 11.5.3, 11.4.x <= 11.4.4, 10.11.x <= 10.11.14 fail to properly validate msgpack-encoded WebSocket frames before memory allocation which allows an unauthenticated remote attacker to crash the server process and cause a full service outage for all users via a crafted binary WebSocket message sent to the public WebSocket endpoint.. Mattermost Advisory ID: MMSA-2026-00647

Affected products

1
  • Range: (11.4.x <= 11.4.4) || (11.5.x <= 11.5.3) || (11.6.x <= 11.6.0) || (10.11.x <= 10.11.14)

Patches

10
29dd6d0cfdb7

MM-68369: add missing TearDown in TestWebConnRejectBinaryFrameUnauthenticated (#36172)

https://github.com/mattermost/mattermostAlejandro García MontoroApr 17, 2026Fixed in 10.11.15via llm-release-walk
1 file changed · +1 0
  • server/channels/app/platform/web_conn_test.go+1 0 modified
    @@ -245,6 +245,7 @@ func TestWebConnDrainDeadQueue(t *testing.T) {
     
     func TestWebConnRejectBinaryFrameUnauthenticated(t *testing.T) {
     	th := Setup(t)
    +	defer th.TearDown()
     
     	readPumpDone := make(chan struct{})
     	upgradeErrCh := make(chan error, 1)
    
3b21498788a7

Added nil checks (#35755) (#36133)

https://github.com/mattermost/mattermostHarshil SharmaApr 16, 2026Fixed in 11.5.4via llm-release-walk
3 files changed · +250 7
  • server/channels/app/channel.go+6 6 modified
    @@ -1605,6 +1605,12 @@ func (a *App) DeleteChannel(rctx request.CTX, channel *model.Channel, userID str
     		return err
     	}
     
    +	deleteAt := model.GetMillis()
    +
    +	if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
    +		return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
    +	}
    +
     	if user != nil {
     		T := i18n.GetUserTranslations(user.Locale)
     
    @@ -1660,12 +1666,6 @@ func (a *App) DeleteChannel(rctx request.CTX, channel *model.Channel, userID str
     		return model.NewAppError("DeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
     	}
     
    -	deleteAt := model.GetMillis()
    -
    -	if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
    -		return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
    -	}
    -
     	a.Srv().Platform().InvalidateCacheForChannel(channel)
     
     	var message *model.WebSocketEvent
    
  • server/channels/app/post_persistent_notification.go+26 1 modified
    @@ -163,13 +163,25 @@ func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(pos
     		return err
     	}
     
    +	var postsForPersistentNotificationCleanup []*model.Post
    +
     	for _, post := range posts {
     		channel := channelsMap[post.ChannelId]
    +		if channel == nil {
    +			postsForPersistentNotificationCleanup = append(postsForPersistentNotificationCleanup, post)
    +			continue
    +		}
    +
     		team := teamsMap[channel.TeamId]
     		// GMs and DMs don't belong to any team
     		if channel.IsGroupOrDirect() {
     			team = &model.Team{}
    +		} else if team == nil {
    +			// cleanup persistent notification for posts with missing teams when they are not DM or GM
    +			postsForPersistentNotificationCleanup = append(postsForPersistentNotificationCleanup, post)
    +			continue
     		}
    +
     		profileMap := channelProfileMap[channel.Id]
     
     		// Ensure the sender is always in the profile map: for example, system admins can post
    @@ -210,6 +222,14 @@ func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(pos
     		}
     	}
     
    +	if len(postsForPersistentNotificationCleanup) > 0 {
    +		for _, post := range postsForPersistentNotificationCleanup {
    +			if appErr := a.DeletePersistentNotification(request.EmptyContext(a.Log()), post); appErr != nil {
    +				a.Log().Warn("Failed to delete persistent notification for post", mlog.String("post_id", post.Id), mlog.String("channel_id", post.ChannelId), mlog.Err(appErr))
    +			}
    +		}
    +	}
    +
     	return nil
     }
     
    @@ -219,9 +239,14 @@ func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model
     	channelKeywords := make(map[string]MentionKeywords, len(channelsMap))
     	channelNotifyProps := make(map[string]map[string]model.StringMap, len(channelsMap))
     	for _, c := range channelsMap {
    +		team := teamsMap[c.TeamId]
    +		if team == nil && !c.IsGroupOrDirect() {
    +			continue
    +		}
    +
     		// In DM, notifications can't be send to any 3rd person.
     		if c.Type != model.ChannelTypeDirect {
    -			groups, err := a.getGroupsAllowedForReferenceInChannel(c, teamsMap[c.TeamId])
    +			groups, err := a.getGroupsAllowedForReferenceInChannel(c, team)
     			if err != nil {
     				return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
     			}
    
  • server/channels/app/post_persistent_notification_test.go+218 0 modified
    @@ -192,6 +192,224 @@ func TestDeletePersistentNotification(t *testing.T) {
     	})
     }
     
    +func TestForEachPersistentNotificationPost(t *testing.T) {
    +	mainHelper.Parallel(t)
    +
    +	t.Run("should cleanup posts whose channel no longer exists", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		profileMap := map[string]*model.User{user1.Id: user1}
    +		team := &model.Team{Id: "tid"}
    +		channel := &model.Channel{Id: "chid", TeamId: team.Id, Type: model.ChannelTypeOpen}
    +
    +		// post1 belongs to an existing channel; post2 belongs to a deleted/missing channel
    +		post1 := &model.Post{Id: "pid1", ChannelId: channel.Id, Message: "hello @user-1", UserId: user1.Id}
    +		post2 := &model.Post{Id: "pid2", ChannelId: "deleted-channel-id", Message: "hello", UserId: user1.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		// Only return channel for post1; post2's channel is missing
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{channel}, nil)
    +		mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{team}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		// DeletePersistentNotification mocks - the cleanup path calls GetSingle then Delete
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +		mockPostPersistentNotification.On("GetSingle", post2.Id).Return(&model.PostPersistentNotifications{PostId: post2.Id}, nil)
    +		mockPostPersistentNotification.On("Delete", []string{post2.Id}).Return(nil)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1, post2}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should only be called for post1 (valid channel)
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// post2 persistent notification should have been cleaned up
    +		mockPostPersistentNotification.AssertCalled(t, "Delete", []string{post2.Id})
    +	})
    +
    +	t.Run("should cleanup posts whose team no longer exists", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		user2 := &model.User{Id: "uid2", Username: "user-2"}
    +		profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2}
    +		team := &model.Team{Id: "tid"}
    +		channel := &model.Channel{Id: "chid", TeamId: team.Id, Type: model.ChannelTypeOpen}
    +		// channelWithMissingTeam has a TeamId that won't be in teamsMap
    +		channelWithMissingTeam := &model.Channel{Id: "chid2", TeamId: "deleted-team-id", Type: model.ChannelTypeOpen}
    +
    +		post1 := &model.Post{Id: "pid1", ChannelId: channel.Id, Message: "hello @user-1", UserId: user2.Id}
    +		post2 := &model.Post{Id: "pid2", ChannelId: channelWithMissingTeam.Id, Message: "hello @user-1", UserId: user2.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		// Both channels exist, but only one team exists
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{channel, channelWithMissingTeam}, nil)
    +		mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		// Only return the team for channel, not for channelWithMissingTeam
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{team}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +		mockPostPersistentNotification.On("GetSingle", post2.Id).Return(&model.PostPersistentNotifications{PostId: post2.Id}, nil)
    +		mockPostPersistentNotification.On("Delete", []string{post2.Id}).Return(nil)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1, post2}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should only be called for post1 (valid team)
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// post2 persistent notification should have been cleaned up due to missing team
    +		mockPostPersistentNotification.AssertCalled(t, "Delete", []string{post2.Id})
    +	})
    +
    +	t.Run("should not cleanup DM posts that have no team", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		user2 := &model.User{Id: "uid2", Username: "user-2"}
    +		profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2}
    +		dmChannel := &model.Channel{Id: "dm-chid", TeamId: "", Type: model.ChannelTypeDirect, Name: model.GetDMNameFromIds(user1.Id, user2.Id)}
    +
    +		post1 := &model.Post{Id: "pid1", ChannelId: dmChannel.Id, Message: "hello", UserId: user1.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{dmChannel}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should be called for the DM post even though there's no team
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// Delete should NOT have been called — DMs don't need a team
    +		mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
    +	})
    +
    +	t.Run("should not cleanup GM posts that have no team", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		user2 := &model.User{Id: "uid2", Username: "user-2"}
    +		user3 := &model.User{Id: "uid3", Username: "user-3"}
    +		profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2, user3.Id: user3}
    +		gmChannel := &model.Channel{Id: "gm-chid", TeamId: "", Type: model.ChannelTypeGroup}
    +
    +		post1 := &model.Post{Id: "pid1", ChannelId: gmChannel.Id, Message: "hello @user-2", UserId: user1.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{gmChannel}, nil)
    +		mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should be called for the GM post even though there's no team
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// Delete should NOT have been called — GMs don't need a team
    +		mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
    +	})
    +}
    +
     func TestSendPersistentNotifications(t *testing.T) {
     	mainHelper.Parallel(t)
     	th := Setup(t).InitBasic(t)
    
2f2dabe523f8

Added nil checks (#35755) (#36132)

https://github.com/mattermost/mattermostHarshil SharmaApr 16, 2026Fixed in 11.6.1via llm-release-walk
3 files changed · +249 7
  • server/channels/app/channel.go+5 6 modified
    @@ -1605,6 +1605,11 @@ func (a *App) DeleteChannel(rctx request.CTX, channel *model.Channel, userID str
     		return err
     	}
     
    +	deleteAt := model.GetMillis()
    +
    +	if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
    +		return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
    +	}
     	if user != nil {
     		T := i18n.GetUserTranslations(user.Locale)
     
    @@ -1660,12 +1665,6 @@ func (a *App) DeleteChannel(rctx request.CTX, channel *model.Channel, userID str
     		return model.NewAppError("DeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
     	}
     
    -	deleteAt := model.GetMillis()
    -
    -	if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
    -		return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
    -	}
    -
     	a.Srv().Platform().InvalidateCacheForChannel(channel)
     
     	var message *model.WebSocketEvent
    
  • server/channels/app/post_persistent_notification.go+26 1 modified
    @@ -163,13 +163,25 @@ func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(pos
     		return err
     	}
     
    +	var postsForPersistentNotificationCleanup []*model.Post
    +
     	for _, post := range posts {
     		channel := channelsMap[post.ChannelId]
    +		if channel == nil {
    +			postsForPersistentNotificationCleanup = append(postsForPersistentNotificationCleanup, post)
    +			continue
    +		}
    +
     		team := teamsMap[channel.TeamId]
     		// GMs and DMs don't belong to any team
     		if channel.IsGroupOrDirect() {
     			team = &model.Team{}
    +		} else if team == nil {
    +			// cleanup persistent notification for posts with missing teams when they are not DM or GM
    +			postsForPersistentNotificationCleanup = append(postsForPersistentNotificationCleanup, post)
    +			continue
     		}
    +
     		profileMap := channelProfileMap[channel.Id]
     
     		// Ensure the sender is always in the profile map: for example, system admins can post
    @@ -210,6 +222,14 @@ func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(pos
     		}
     	}
     
    +	if len(postsForPersistentNotificationCleanup) > 0 {
    +		for _, post := range postsForPersistentNotificationCleanup {
    +			if appErr := a.DeletePersistentNotification(request.EmptyContext(a.Log()), post); appErr != nil {
    +				a.Log().Warn("Failed to delete persistent notification for post", mlog.String("post_id", post.Id), mlog.String("channel_id", post.ChannelId), mlog.Err(appErr))
    +			}
    +		}
    +	}
    +
     	return nil
     }
     
    @@ -219,9 +239,14 @@ func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model
     	channelKeywords := make(map[string]MentionKeywords, len(channelsMap))
     	channelNotifyProps := make(map[string]map[string]model.StringMap, len(channelsMap))
     	for _, c := range channelsMap {
    +		team := teamsMap[c.TeamId]
    +		if team == nil && !c.IsGroupOrDirect() {
    +			continue
    +		}
    +
     		// In DM, notifications can't be send to any 3rd person.
     		if c.Type != model.ChannelTypeDirect {
    -			groups, err := a.getGroupsAllowedForReferenceInChannel(c, teamsMap[c.TeamId])
    +			groups, err := a.getGroupsAllowedForReferenceInChannel(c, team)
     			if err != nil {
     				return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
     			}
    
  • server/channels/app/post_persistent_notification_test.go+218 0 modified
    @@ -192,6 +192,224 @@ func TestDeletePersistentNotification(t *testing.T) {
     	})
     }
     
    +func TestForEachPersistentNotificationPost(t *testing.T) {
    +	mainHelper.Parallel(t)
    +
    +	t.Run("should cleanup posts whose channel no longer exists", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		profileMap := map[string]*model.User{user1.Id: user1}
    +		team := &model.Team{Id: "tid"}
    +		channel := &model.Channel{Id: "chid", TeamId: team.Id, Type: model.ChannelTypeOpen}
    +
    +		// post1 belongs to an existing channel; post2 belongs to a deleted/missing channel
    +		post1 := &model.Post{Id: "pid1", ChannelId: channel.Id, Message: "hello @user-1", UserId: user1.Id}
    +		post2 := &model.Post{Id: "pid2", ChannelId: "deleted-channel-id", Message: "hello", UserId: user1.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		// Only return channel for post1; post2's channel is missing
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{channel}, nil)
    +		mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{team}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		// DeletePersistentNotification mocks - the cleanup path calls GetSingle then Delete
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +		mockPostPersistentNotification.On("GetSingle", post2.Id).Return(&model.PostPersistentNotifications{PostId: post2.Id}, nil)
    +		mockPostPersistentNotification.On("Delete", []string{post2.Id}).Return(nil)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1, post2}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should only be called for post1 (valid channel)
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// post2 persistent notification should have been cleaned up
    +		mockPostPersistentNotification.AssertCalled(t, "Delete", []string{post2.Id})
    +	})
    +
    +	t.Run("should cleanup posts whose team no longer exists", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		user2 := &model.User{Id: "uid2", Username: "user-2"}
    +		profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2}
    +		team := &model.Team{Id: "tid"}
    +		channel := &model.Channel{Id: "chid", TeamId: team.Id, Type: model.ChannelTypeOpen}
    +		// channelWithMissingTeam has a TeamId that won't be in teamsMap
    +		channelWithMissingTeam := &model.Channel{Id: "chid2", TeamId: "deleted-team-id", Type: model.ChannelTypeOpen}
    +
    +		post1 := &model.Post{Id: "pid1", ChannelId: channel.Id, Message: "hello @user-1", UserId: user2.Id}
    +		post2 := &model.Post{Id: "pid2", ChannelId: channelWithMissingTeam.Id, Message: "hello @user-1", UserId: user2.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		// Both channels exist, but only one team exists
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{channel, channelWithMissingTeam}, nil)
    +		mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		// Only return the team for channel, not for channelWithMissingTeam
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{team}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +		mockPostPersistentNotification.On("GetSingle", post2.Id).Return(&model.PostPersistentNotifications{PostId: post2.Id}, nil)
    +		mockPostPersistentNotification.On("Delete", []string{post2.Id}).Return(nil)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1, post2}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should only be called for post1 (valid team)
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// post2 persistent notification should have been cleaned up due to missing team
    +		mockPostPersistentNotification.AssertCalled(t, "Delete", []string{post2.Id})
    +	})
    +
    +	t.Run("should not cleanup DM posts that have no team", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		user2 := &model.User{Id: "uid2", Username: "user-2"}
    +		profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2}
    +		dmChannel := &model.Channel{Id: "dm-chid", TeamId: "", Type: model.ChannelTypeDirect, Name: model.GetDMNameFromIds(user1.Id, user2.Id)}
    +
    +		post1 := &model.Post{Id: "pid1", ChannelId: dmChannel.Id, Message: "hello", UserId: user1.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{dmChannel}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should be called for the DM post even though there's no team
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// Delete should NOT have been called — DMs don't need a team
    +		mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
    +	})
    +
    +	t.Run("should not cleanup GM posts that have no team", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		user2 := &model.User{Id: "uid2", Username: "user-2"}
    +		user3 := &model.User{Id: "uid3", Username: "user-3"}
    +		profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2, user3.Id: user3}
    +		gmChannel := &model.Channel{Id: "gm-chid", TeamId: "", Type: model.ChannelTypeGroup}
    +
    +		post1 := &model.Post{Id: "pid1", ChannelId: gmChannel.Id, Message: "hello @user-2", UserId: user1.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{gmChannel}, nil)
    +		mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should be called for the GM post even though there's no team
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// Delete should NOT have been called — GMs don't need a team
    +		mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
    +	})
    +}
    +
     func TestSendPersistentNotifications(t *testing.T) {
     	mainHelper.Parallel(t)
     	th := Setup(t).InitBasic(t)
    
c7aff1fcd9c3

Update msgpack fork dependency (#35988) (#36042)

https://github.com/mattermost/mattermostCaleb RoselandApr 16, 2026Fixed in 11.4.5via llm-release-walk
4 files changed · +56 4
  • server/channels/app/platform/web_conn.go+6 0 modified
    @@ -469,6 +469,12 @@ func (wc *WebConn) readPump() {
     			return
     		}
     
    +		// Reject binary frames from unauthenticated connections. See MM-68222.
    +		if msgType != websocket.TextMessage && !wc.IsAuthenticated() {
    +			wc.logSocketErr("websocket.UnauthBinary", errors.New("binary frames require authentication"))
    +			return
    +		}
    +
     		var decoder interface {
     			Decode(v any) error
     		}
    
  • server/channels/app/platform/web_conn_test.go+46 0 modified
    @@ -10,6 +10,7 @@ import (
     	"net/http"
     	"net/http/httptest"
     	"testing"
    +	"time"
     
     	"github.com/gorilla/websocket"
     	"github.com/stretchr/testify/assert"
    @@ -237,3 +238,48 @@ func TestWebConnDrainDeadQueue(t *testing.T) {
     		t.Run("Overwritten First", func(t *testing.T) { run(int64(128), deadQueueSize+10) })
     	})
     }
    +
    +func TestWebConnRejectBinaryFrameUnauthenticated(t *testing.T) {
    +	th := Setup(t)
    +
    +	readPumpDone := make(chan struct{})
    +	upgradeErrCh := make(chan error, 1)
    +
    +	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		upgrader := &websocket.Upgrader{}
    +		conn, err := upgrader.Upgrade(w, r, nil)
    +		if err != nil {
    +			upgradeErrCh <- err
    +			return
    +		}
    +		upgradeErrCh <- nil
    +
    +		wc := th.Service.NewWebConn(&WebConnConfig{
    +			WebSocket: conn,
    +		}, th.Suite, &hookRunner{})
    +
    +		require.False(t, wc.IsAuthenticated())
    +
    +		go func() {
    +			wc.readPump()
    +			close(readPumpDone)
    +		}()
    +	}))
    +	defer s.Close()
    +
    +	d := websocket.Dialer{}
    +	clientConn, _, err := d.Dial("ws://"+s.Listener.Addr().String()+"/ws", nil)
    +	require.NoError(t, err)
    +	defer clientConn.Close()
    +
    +	require.NoError(t, <-upgradeErrCh)
    +
    +	err = clientConn.WriteMessage(websocket.BinaryMessage, []byte{0x01, 0x02, 0x03})
    +	require.NoError(t, err)
    +
    +	select {
    +	case <-readPumpDone:
    +	case <-time.After(5 * time.Second):
    +		require.Fail(t, "readPump did not exit after receiving binary frame")
    +	}
    +}
    
  • server/go.mod+2 2 modified
    @@ -230,5 +230,5 @@ require (
     // https://github.com/advancedlogic/GoOse/pull/77
     replace github.com/olekukonko/tablewriter => github.com/olekukonko/tablewriter v0.0.5
     
    -// See MM-66167 for more details.
    -replace github.com/vmihailenco/msgpack/v5 => github.com/mattermost/msgpack/v5 v5.0.0-20260120151306-2f9c67d7e57f
    +// See MM-66167, MM-68222 for more details.
    +replace github.com/vmihailenco/msgpack/v5 => github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815
    
  • server/go.sum+2 2 modified
    @@ -411,8 +411,8 @@ github.com/mattermost/mattermost/server/public v0.1.20 h1:N39ZOyYvCVZXABetpb0tVr
     github.com/mattermost/mattermost/server/public v0.1.20/go.mod h1:ZSdLCHIFYDTmX5FNCJ7LoI2CLZ1rQqZcfJ3ZF81QadY=
     github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6Cw=
     github.com/mattermost/morph v1.1.0/go.mod h1:gD+EaqX2UMyyuzmF4PFh4r33XneQ8Nzi+0E8nXjMa3A=
    -github.com/mattermost/msgpack/v5 v5.0.0-20260120151306-2f9c67d7e57f h1:tAXeRJSWo6EK7wDq1TcxMHIxHRyjrE62ihvsigdg4Q0=
    -github.com/mattermost/msgpack/v5 v5.0.0-20260120151306-2f9c67d7e57f/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
    +github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815 h1:uOi89NvrFmDngqMKjlLDxi+MNzJQLA3TqcU2p8czv34=
    +github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
     github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 h1:G9tL6JXRBMzjuD1kkBtcnd42kUiT6QDwxfFYu7adM6o=
     github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs=
     github.com/mattermost/squirrel v0.5.0 h1:81QPS0aA+inQbpA7Pzmv6O9sWwB6VaBh/VYw3oJf8ZY=
    
384635216f46

Update msgpack fork dependency (#35988) (#36043)

https://github.com/mattermost/mattermostCaleb RoselandApr 16, 2026Fixed in 10.11.15via llm-release-walk
4 files changed · +56 4
  • server/channels/app/platform/web_conn.go+6 0 modified
    @@ -469,6 +469,12 @@ func (wc *WebConn) readPump() {
     			return
     		}
     
    +		// Reject binary frames from unauthenticated connections. See MM-68222.
    +		if msgType != websocket.TextMessage && !wc.IsAuthenticated() {
    +			wc.logSocketErr("websocket.UnauthBinary", errors.New("binary frames require authentication"))
    +			return
    +		}
    +
     		var decoder interface {
     			Decode(v any) error
     		}
    
  • server/channels/app/platform/web_conn_test.go+46 0 modified
    @@ -10,6 +10,7 @@ import (
     	"net/http"
     	"net/http/httptest"
     	"testing"
    +	"time"
     
     	"github.com/gorilla/websocket"
     	"github.com/stretchr/testify/assert"
    @@ -241,3 +242,48 @@ func TestWebConnDrainDeadQueue(t *testing.T) {
     		t.Run("Overwritten First", func(t *testing.T) { run(int64(128), deadQueueSize+10) })
     	})
     }
    +
    +func TestWebConnRejectBinaryFrameUnauthenticated(t *testing.T) {
    +	th := Setup(t)
    +
    +	readPumpDone := make(chan struct{})
    +	upgradeErrCh := make(chan error, 1)
    +
    +	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		upgrader := &websocket.Upgrader{}
    +		conn, err := upgrader.Upgrade(w, r, nil)
    +		if err != nil {
    +			upgradeErrCh <- err
    +			return
    +		}
    +		upgradeErrCh <- nil
    +
    +		wc := th.Service.NewWebConn(&WebConnConfig{
    +			WebSocket: conn,
    +		}, th.Suite, &hookRunner{})
    +
    +		require.False(t, wc.IsAuthenticated())
    +
    +		go func() {
    +			wc.readPump()
    +			close(readPumpDone)
    +		}()
    +	}))
    +	defer s.Close()
    +
    +	d := websocket.Dialer{}
    +	clientConn, _, err := d.Dial("ws://"+s.Listener.Addr().String()+"/ws", nil)
    +	require.NoError(t, err)
    +	defer clientConn.Close()
    +
    +	require.NoError(t, <-upgradeErrCh)
    +
    +	err = clientConn.WriteMessage(websocket.BinaryMessage, []byte{0x01, 0x02, 0x03})
    +	require.NoError(t, err)
    +
    +	select {
    +	case <-readPumpDone:
    +	case <-time.After(5 * time.Second):
    +		require.Fail(t, "readPump did not exit after receiving binary frame")
    +	}
    +}
    
  • server/go.mod+2 2 modified
    @@ -257,5 +257,5 @@ replace github.com/ledongthuc/pdf => github.com/ledongthuc/pdf v0.0.0-2024020113
     // version always. Tablewriter has made breaking changes to its latest release.
     replace github.com/olekukonko/tablewriter => github.com/olekukonko/tablewriter v0.0.5
     
    -// See MM-66167 for more details.
    -replace github.com/vmihailenco/msgpack/v5 => github.com/mattermost/msgpack/v5 v5.0.0-20260120151306-2f9c67d7e57f
    +// See MM-66167, MM-68222 for more details.
    +replace github.com/vmihailenco/msgpack/v5 => github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815
    
  • server/go.sum+2 2 modified
    @@ -423,8 +423,8 @@ github.com/mattermost/mattermost/server/public v0.1.12 h1:qlIU/llY0FWdHWQPtvncdd
     github.com/mattermost/mattermost/server/public v0.1.12/go.mod h1:3RJZfl7sMedX6ihX+JMFOIAzCHhd0WQnuez+UFQS80k=
     github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6Cw=
     github.com/mattermost/morph v1.1.0/go.mod h1:gD+EaqX2UMyyuzmF4PFh4r33XneQ8Nzi+0E8nXjMa3A=
    -github.com/mattermost/msgpack/v5 v5.0.0-20260120151306-2f9c67d7e57f h1:tAXeRJSWo6EK7wDq1TcxMHIxHRyjrE62ihvsigdg4Q0=
    -github.com/mattermost/msgpack/v5 v5.0.0-20260120151306-2f9c67d7e57f/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
    +github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815 h1:uOi89NvrFmDngqMKjlLDxi+MNzJQLA3TqcU2p8czv34=
    +github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
     github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 h1:G9tL6JXRBMzjuD1kkBtcnd42kUiT6QDwxfFYu7adM6o=
     github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs=
     github.com/mattermost/squirrel v0.5.0 h1:81QPS0aA+inQbpA7Pzmv6O9sWwB6VaBh/VYw3oJf8ZY=
    
c4960b893bf0

Update msgpack fork dependency (#35988) (#36040)

https://github.com/mattermost/mattermostCaleb RoselandApr 13, 2026Fixed in 11.6.1via llm-release-walk
4 files changed · +56 4
  • server/channels/app/platform/web_conn.go+6 0 modified
    @@ -469,6 +469,12 @@ func (wc *WebConn) readPump() {
     			return
     		}
     
    +		// Reject binary frames from unauthenticated connections. See MM-68222.
    +		if msgType != websocket.TextMessage && !wc.IsAuthenticated() {
    +			wc.logSocketErr("websocket.UnauthBinary", errors.New("binary frames require authentication"))
    +			return
    +		}
    +
     		var decoder interface {
     			Decode(v any) error
     		}
    
  • server/channels/app/platform/web_conn_test.go+46 0 modified
    @@ -10,6 +10,7 @@ import (
     	"net/http"
     	"net/http/httptest"
     	"testing"
    +	"time"
     
     	"github.com/gorilla/websocket"
     	"github.com/stretchr/testify/assert"
    @@ -237,3 +238,48 @@ func TestWebConnDrainDeadQueue(t *testing.T) {
     		t.Run("Overwritten First", func(t *testing.T) { run(int64(128), deadQueueSize+10) })
     	})
     }
    +
    +func TestWebConnRejectBinaryFrameUnauthenticated(t *testing.T) {
    +	th := Setup(t)
    +
    +	readPumpDone := make(chan struct{})
    +	upgradeErrCh := make(chan error, 1)
    +
    +	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +		upgrader := &websocket.Upgrader{}
    +		conn, err := upgrader.Upgrade(w, r, nil)
    +		if err != nil {
    +			upgradeErrCh <- err
    +			return
    +		}
    +		upgradeErrCh <- nil
    +
    +		wc := th.Service.NewWebConn(&WebConnConfig{
    +			WebSocket: conn,
    +		}, th.Suite, &hookRunner{})
    +
    +		require.False(t, wc.IsAuthenticated())
    +
    +		go func() {
    +			wc.readPump()
    +			close(readPumpDone)
    +		}()
    +	}))
    +	defer s.Close()
    +
    +	d := websocket.Dialer{}
    +	clientConn, _, err := d.Dial("ws://"+s.Listener.Addr().String()+"/ws", nil)
    +	require.NoError(t, err)
    +	defer clientConn.Close()
    +
    +	require.NoError(t, <-upgradeErrCh)
    +
    +	err = clientConn.WriteMessage(websocket.BinaryMessage, []byte{0x01, 0x02, 0x03})
    +	require.NoError(t, err)
    +
    +	select {
    +	case <-readPumpDone:
    +	case <-time.After(5 * time.Second):
    +		require.Fail(t, "readPump did not exit after receiving binary frame")
    +	}
    +}
    
  • server/go.mod+2 2 modified
    @@ -231,5 +231,5 @@ require (
     // https://github.com/advancedlogic/GoOse/pull/77
     replace github.com/olekukonko/tablewriter => github.com/olekukonko/tablewriter v0.0.5
     
    -// See MM-66167 for more details.
    -replace github.com/vmihailenco/msgpack/v5 => github.com/mattermost/msgpack/v5 v5.0.0-20260120151306-2f9c67d7e57f
    +// See MM-66167, MM-68222 for more details.
    +replace github.com/vmihailenco/msgpack/v5 => github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815
    
  • server/go.sum+2 2 modified
    @@ -415,8 +415,8 @@ github.com/mattermost/mattermost/server/public v0.1.22-0.20251105210629-8bf4a007
     github.com/mattermost/mattermost/server/public v0.1.22-0.20251105210629-8bf4a00724e2/go.mod h1:X0RG3lk0XK0SFSH67JS/xporlz3TxItHEPlFIrsQIa8=
     github.com/mattermost/morph v1.1.0 h1:Q9vrJbeM3s2jfweGheq12EFIzdNp9a/6IovcbvOQ6Cw=
     github.com/mattermost/morph v1.1.0/go.mod h1:gD+EaqX2UMyyuzmF4PFh4r33XneQ8Nzi+0E8nXjMa3A=
    -github.com/mattermost/msgpack/v5 v5.0.0-20260120151306-2f9c67d7e57f h1:tAXeRJSWo6EK7wDq1TcxMHIxHRyjrE62ihvsigdg4Q0=
    -github.com/mattermost/msgpack/v5 v5.0.0-20260120151306-2f9c67d7e57f/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
    +github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815 h1:uOi89NvrFmDngqMKjlLDxi+MNzJQLA3TqcU2p8czv34=
    +github.com/mattermost/msgpack/v5 v5.0.0-20260408165622-cadfad56a815/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
     github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0 h1:G9tL6JXRBMzjuD1kkBtcnd42kUiT6QDwxfFYu7adM6o=
     github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs=
     github.com/mattermost/squirrel v0.5.0 h1:81QPS0aA+inQbpA7Pzmv6O9sWwB6VaBh/VYw3oJf8ZY=
    
a07e4b05ff82

Added nil checks (#35755) (#36134)

https://github.com/mattermost/mattermostHarshil SharmaApr 16, 2026Fixed in 11.4.5via llm-release-walk
3 files changed · +250 7
  • server/channels/app/channel.go+6 6 modified
    @@ -1580,6 +1580,12 @@ func (a *App) DeleteChannel(rctx request.CTX, channel *model.Channel, userID str
     		return err
     	}
     
    +	deleteAt := model.GetMillis()
    +
    +	if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
    +		return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
    +	}
    +
     	if user != nil {
     		T := i18n.GetUserTranslations(user.Locale)
     
    @@ -1635,12 +1641,6 @@ func (a *App) DeleteChannel(rctx request.CTX, channel *model.Channel, userID str
     		return model.NewAppError("DeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
     	}
     
    -	deleteAt := model.GetMillis()
    -
    -	if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
    -		return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
    -	}
    -
     	a.Srv().Platform().InvalidateCacheForChannel(channel)
     
     	var message *model.WebSocketEvent
    
  • server/channels/app/post_persistent_notification.go+26 1 modified
    @@ -163,13 +163,25 @@ func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(pos
     		return err
     	}
     
    +	var postsForPersistentNotificationCleanup []*model.Post
    +
     	for _, post := range posts {
     		channel := channelsMap[post.ChannelId]
    +		if channel == nil {
    +			postsForPersistentNotificationCleanup = append(postsForPersistentNotificationCleanup, post)
    +			continue
    +		}
    +
     		team := teamsMap[channel.TeamId]
     		// GMs and DMs don't belong to any team
     		if channel.IsGroupOrDirect() {
     			team = &model.Team{}
    +		} else if team == nil {
    +			// cleanup persistent notification for posts with missing teams when they are not DM or GM
    +			postsForPersistentNotificationCleanup = append(postsForPersistentNotificationCleanup, post)
    +			continue
     		}
    +
     		profileMap := channelProfileMap[channel.Id]
     
     		// Ensure the sender is always in the profile map: for example, system admins can post
    @@ -210,6 +222,14 @@ func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(pos
     		}
     	}
     
    +	if len(postsForPersistentNotificationCleanup) > 0 {
    +		for _, post := range postsForPersistentNotificationCleanup {
    +			if appErr := a.DeletePersistentNotification(request.EmptyContext(a.Log()), post); appErr != nil {
    +				a.Log().Warn("Failed to delete persistent notification for post", mlog.String("post_id", post.Id), mlog.String("channel_id", post.ChannelId), mlog.Err(appErr))
    +			}
    +		}
    +	}
    +
     	return nil
     }
     
    @@ -219,9 +239,14 @@ func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model
     	channelKeywords := make(map[string]MentionKeywords, len(channelsMap))
     	channelNotifyProps := make(map[string]map[string]model.StringMap, len(channelsMap))
     	for _, c := range channelsMap {
    +		team := teamsMap[c.TeamId]
    +		if team == nil && !c.IsGroupOrDirect() {
    +			continue
    +		}
    +
     		// In DM, notifications can't be send to any 3rd person.
     		if c.Type != model.ChannelTypeDirect {
    -			groups, err := a.getGroupsAllowedForReferenceInChannel(c, teamsMap[c.TeamId])
    +			groups, err := a.getGroupsAllowedForReferenceInChannel(c, team)
     			if err != nil {
     				return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
     			}
    
  • server/channels/app/post_persistent_notification_test.go+218 0 modified
    @@ -192,6 +192,224 @@ func TestDeletePersistentNotification(t *testing.T) {
     	})
     }
     
    +func TestForEachPersistentNotificationPost(t *testing.T) {
    +	mainHelper.Parallel(t)
    +
    +	t.Run("should cleanup posts whose channel no longer exists", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		profileMap := map[string]*model.User{user1.Id: user1}
    +		team := &model.Team{Id: "tid"}
    +		channel := &model.Channel{Id: "chid", TeamId: team.Id, Type: model.ChannelTypeOpen}
    +
    +		// post1 belongs to an existing channel; post2 belongs to a deleted/missing channel
    +		post1 := &model.Post{Id: "pid1", ChannelId: channel.Id, Message: "hello @user-1", UserId: user1.Id}
    +		post2 := &model.Post{Id: "pid2", ChannelId: "deleted-channel-id", Message: "hello", UserId: user1.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		// Only return channel for post1; post2's channel is missing
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{channel}, nil)
    +		mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{team}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		// DeletePersistentNotification mocks - the cleanup path calls GetSingle then Delete
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +		mockPostPersistentNotification.On("GetSingle", post2.Id).Return(&model.PostPersistentNotifications{PostId: post2.Id}, nil)
    +		mockPostPersistentNotification.On("Delete", []string{post2.Id}).Return(nil)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1, post2}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should only be called for post1 (valid channel)
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// post2 persistent notification should have been cleaned up
    +		mockPostPersistentNotification.AssertCalled(t, "Delete", []string{post2.Id})
    +	})
    +
    +	t.Run("should cleanup posts whose team no longer exists", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		user2 := &model.User{Id: "uid2", Username: "user-2"}
    +		profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2}
    +		team := &model.Team{Id: "tid"}
    +		channel := &model.Channel{Id: "chid", TeamId: team.Id, Type: model.ChannelTypeOpen}
    +		// channelWithMissingTeam has a TeamId that won't be in teamsMap
    +		channelWithMissingTeam := &model.Channel{Id: "chid2", TeamId: "deleted-team-id", Type: model.ChannelTypeOpen}
    +
    +		post1 := &model.Post{Id: "pid1", ChannelId: channel.Id, Message: "hello @user-1", UserId: user2.Id}
    +		post2 := &model.Post{Id: "pid2", ChannelId: channelWithMissingTeam.Id, Message: "hello @user-1", UserId: user2.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		// Both channels exist, but only one team exists
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{channel, channelWithMissingTeam}, nil)
    +		mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		// Only return the team for channel, not for channelWithMissingTeam
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{team}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +		mockPostPersistentNotification.On("GetSingle", post2.Id).Return(&model.PostPersistentNotifications{PostId: post2.Id}, nil)
    +		mockPostPersistentNotification.On("Delete", []string{post2.Id}).Return(nil)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1, post2}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should only be called for post1 (valid team)
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// post2 persistent notification should have been cleaned up due to missing team
    +		mockPostPersistentNotification.AssertCalled(t, "Delete", []string{post2.Id})
    +	})
    +
    +	t.Run("should not cleanup DM posts that have no team", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		user2 := &model.User{Id: "uid2", Username: "user-2"}
    +		profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2}
    +		dmChannel := &model.Channel{Id: "dm-chid", TeamId: "", Type: model.ChannelTypeDirect, Name: model.GetDMNameFromIds(user1.Id, user2.Id)}
    +
    +		post1 := &model.Post{Id: "pid1", ChannelId: dmChannel.Id, Message: "hello", UserId: user1.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{dmChannel}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should be called for the DM post even though there's no team
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// Delete should NOT have been called — DMs don't need a team
    +		mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
    +	})
    +
    +	t.Run("should not cleanup GM posts that have no team", func(t *testing.T) {
    +		th := SetupWithStoreMock(t)
    +
    +		user1 := &model.User{Id: "uid1", Username: "user-1"}
    +		user2 := &model.User{Id: "uid2", Username: "user-2"}
    +		user3 := &model.User{Id: "uid3", Username: "user-3"}
    +		profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2, user3.Id: user3}
    +		gmChannel := &model.Channel{Id: "gm-chid", TeamId: "", Type: model.ChannelTypeGroup}
    +
    +		post1 := &model.Post{Id: "pid1", ChannelId: gmChannel.Id, Message: "hello @user-2", UserId: user1.Id}
    +
    +		mockStore := th.App.Srv().Store().(*storemocks.Store)
    +
    +		mockChannel := storemocks.ChannelStore{}
    +		mockStore.On("Channel").Return(&mockChannel)
    +		mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{gmChannel}, nil)
    +		mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
    +
    +		mockTeam := storemocks.TeamStore{}
    +		mockStore.On("Team").Return(&mockTeam)
    +		mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{}, nil)
    +
    +		mockUser := storemocks.UserStore{}
    +		mockStore.On("User").Return(&mockUser)
    +		mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
    +
    +		mockGroup := storemocks.GroupStore{}
    +		mockStore.On("Group").Return(&mockGroup)
    +		mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
    +
    +		mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
    +		mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
    +
    +		th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
    +		cfg := th.App.Config()
    +		*cfg.ServiceSettings.PostPriority = true
    +		*cfg.ServiceSettings.AllowPersistentNotifications = true
    +
    +		fnCalled := []string{}
    +		err := th.App.forEachPersistentNotificationPost([]*model.Post{post1}, func(post *model.Post, _ *model.Channel, _ *model.Team, _ *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
    +			fnCalled = append(fnCalled, post.Id)
    +			return nil
    +		})
    +		require.NoError(t, err)
    +
    +		// The callback should be called for the GM post even though there's no team
    +		assert.Equal(t, []string{"pid1"}, fnCalled)
    +		// Delete should NOT have been called — GMs don't need a team
    +		mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
    +	})
    +}
    +
     func TestSendPersistentNotifications(t *testing.T) {
     	mainHelper.Parallel(t)
     	th := Setup(t).InitBasic(t)
    
a5fb12645247
https://github.com/mattermost/mattermostFixed in 11.4.5via llm-release-walk
ffee10a61081

Bump Boards FIPS version to v9.2.4 (#36165) (#36168)

https://github.com/mattermost/mattermostMattermost BuildApr 17, 2026Fixed in 11.6.1via release-tag
1 file changed · +1 1
  • server/Makefile+1 1 modified
    @@ -183,7 +183,7 @@ PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.3.0
     ifeq ($(FIPS_ENABLED),true)
     	PLUGIN_PACKAGES  = mattermost-plugin-playbooks-v2.8.0%2Bc4449ac-fips
     	PLUGIN_PACKAGES += mattermost-plugin-agents-v1.7.2%2B866e2dd-fips
    -	PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.2%2B4282c63-fips
    +	PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.4%2B5855fe1-fips
     endif
     
     EE_PACKAGES=$(shell $(GO) list $(BUILD_ENTERPRISE_DIR)/...)
    
fff6ab3a5851

Bump Boards FIPS version to v9.2.4 (#36165) (#36169)

https://github.com/mattermost/mattermostMattermost BuildApr 17, 2026Fixed in 11.5.4via release-tag
1 file changed · +1 1
  • server/Makefile+1 1 modified
    @@ -176,7 +176,7 @@ PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.3.0
     ifeq ($(FIPS_ENABLED),true)
     	PLUGIN_PACKAGES  = mattermost-plugin-playbooks-v2.8.0%2Bc4449ac-fips
     	PLUGIN_PACKAGES += mattermost-plugin-agents-v1.7.2%2B866e2dd-fips
    -	PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.2%2B4282c63-fips
    +	PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.4%2B5855fe1-fips
     endif
     
     EE_PACKAGES=$(shell $(GO) list $(BUILD_ENTERPRISE_DIR)/...)
    

Vulnerability mechanics

Root cause

"Missing validation of msgpack-encoded binary WebSocket frames before memory allocation allows an unauthenticated attacker to trigger excessive memory consumption or a nil-pointer dereference, crashing the server."

Attack vector

An unauthenticated remote attacker opens a WebSocket connection to the public WebSocket endpoint and sends a crafted binary message (msgpack-encoded). The server's `readPump()` function in `web_conn.go` previously did not check whether the connection was authenticated before decoding binary frames. Because the msgpack decoder could be fed malformed data without size limits, the server would allocate memory based on attacker-controlled values, leading to a crash and full service outage. No authentication or prior knowledge is required beyond network access to the WebSocket endpoint.

Affected code

The primary vulnerable code is in `server/channels/app/platform/web_conn.go` in the `readPump()` method, where binary WebSocket frames were decoded without checking authentication status. The msgpack decoder dependency (`github.com/vmihailenco/msgpack/v5`, replaced by a Mattermost fork) lacked input validation that could lead to uncontrolled memory allocation. Additional nil-pointer issues exist in `server/channels/app/post_persistent_notification.go` (`forEachPersistentNotificationPost` and `persistentNotificationsAuxiliaryData`) and `server/channels/app/channel.go` (`DeleteChannel`).

What the fix does

The fix adds a guard in `readPump()` (`web_conn.go`) that rejects binary frames from unauthenticated connections: if `msgType != websocket.TextMessage && !wc.IsAuthenticated()`, the connection is closed immediately with a log message [patch_id=1693052][patch_id=1693056][patch_id=1693393]. Additionally, the msgpack fork dependency was updated to a newer version (`v5.0.0-20260408165622-cadfad56a815`) that includes hardening against malformed input [patch_id=1693052][patch_id=1693056][patch_id=1693393]. The nil-check patches in `post_persistent_notification.go` and `channel.go` address a separate but related issue where missing nil checks on channel and team lookups could cause panics [patch_id=1693392][patch_id=1693051][patch_id=1693053].

Preconditions

  • networkNetwork access to the Mattermost public WebSocket endpoint (typically /ws)
  • authNo authentication required — the attack is performed on an unauthenticated WebSocket connection
  • inputThe attacker sends a crafted binary (msgpack-encoded) WebSocket frame

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