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

Guest users can bypass read permissions via search API

CVE-2026-24692

Description

Mattermost versions 11.3.x <= 11.3.0, 11.2.x <= 11.2.2, 10.11.x <= 10.11.10 fail to properly enforce read permissions in search API endpoints which allows guest users without read permissions to access posts and files in channels via search API requests. Mattermost Advisory ID: MMSA-2025-00554

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost/server/v8Go
< 8.0.0-20260107142155-0481bd1fb0458.0.0-20260107142155-0481bd1fb045
github.com/mattermost/mattermost-serverGo
< 5.3.2-0.20260107142155-0481bd1fb0455.3.2-0.20260107142155-0481bd1fb045
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
0481bd1fb045

Fliter post in search api with no read content channel permission (#34620)

https://github.com/mattermost/mattermostRajat DabadeJan 7, 2026via ghsa
4 files changed · +398 1
  • server/channels/app/file.go+66 1 modified
    @@ -22,6 +22,9 @@ import (
     	"sync"
     	"time"
     
    +	"maps"
    +	"slices"
    +
     	"github.com/mattermost/mattermost/server/public/model"
     	"github.com/mattermost/mattermost/server/public/plugin"
     	"github.com/mattermost/mattermost/server/public/shared/mlog"
    @@ -1480,7 +1483,69 @@ func (a *App) SearchFilesInTeamForUser(rctx request.CTX, terms string, userId st
     		}
     	}
     
    -	return fileInfoSearchResults, a.filterInaccessibleFiles(fileInfoSearchResults, filterFileOptions{assumeSortedCreatedAt: true})
    +	if appErr := a.filterInaccessibleFiles(fileInfoSearchResults, filterFileOptions{assumeSortedCreatedAt: true}); appErr != nil {
    +		return nil, appErr
    +	}
    +
    +	if appErr := a.FilterFilesByChannelPermissions(rctx, fileInfoSearchResults, userId); appErr != nil {
    +		return nil, appErr
    +	}
    +
    +	return fileInfoSearchResults, nil
    +}
    +
    +func (a *App) FilterFilesByChannelPermissions(rctx request.CTX, fileList *model.FileInfoList, userID string) *model.AppError {
    +	if fileList == nil || fileList.FileInfos == nil || len(fileList.FileInfos) == 0 {
    +		return nil
    +	}
    +
    +	channels := make(map[string]*model.Channel)
    +	for _, fileInfo := range fileList.FileInfos {
    +		if fileInfo.ChannelId != "" {
    +			channels[fileInfo.ChannelId] = nil
    +		}
    +	}
    +
    +	if len(channels) > 0 {
    +		channelIDs := slices.Collect(maps.Keys(channels))
    +		channelList, err := a.GetChannels(rctx, channelIDs)
    +		if err != nil && err.StatusCode != http.StatusNotFound {
    +			return err
    +		}
    +		for _, channel := range channelList {
    +			channels[channel.Id] = channel
    +		}
    +	}
    +
    +	channelReadPermission := make(map[string]bool)
    +	filteredFiles := make(map[string]*model.FileInfo)
    +	filteredOrder := []string{}
    +
    +	for _, fileID := range fileList.Order {
    +		fileInfo, ok := fileList.FileInfos[fileID]
    +		if !ok {
    +			continue
    +		}
    +
    +		if _, ok := channelReadPermission[fileInfo.ChannelId]; !ok {
    +			channel := channels[fileInfo.ChannelId]
    +			allowed := false
    +			if channel != nil {
    +				allowed = a.HasPermissionToReadChannel(rctx, userID, channel)
    +			}
    +			channelReadPermission[fileInfo.ChannelId] = allowed
    +		}
    +
    +		if channelReadPermission[fileInfo.ChannelId] {
    +			filteredFiles[fileID] = fileInfo
    +			filteredOrder = append(filteredOrder, fileID)
    +		}
    +	}
    +
    +	fileList.FileInfos = filteredFiles
    +	fileList.Order = filteredOrder
    +
    +	return nil
     }
     
     func (a *App) ExtractContentFromFileInfo(rctx request.CTX, fileInfo *model.FileInfo) error {
    
  • server/channels/app/file_test.go+138 0 modified
    @@ -836,3 +836,141 @@ func TestPermanentDeleteFilesByPost(t *testing.T) {
     		assert.Nil(t, err)
     	})
     }
    +
    +func TestFilterFilesByChannelPermissions(t *testing.T) {
    +	mainHelper.Parallel(t)
    +	th := Setup(t).InitBasic(t)
    +
    +	th.App.UpdateConfig(func(cfg *model.Config) {
    +		*cfg.GuestAccountsSettings.Enable = true
    +	})
    +
    +	guestUser := th.CreateGuest(t)
    +	_, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guestUser.Id, "")
    +	require.Nil(t, appErr)
    +
    +	privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
    +
    +	_, appErr = th.App.AddUserToChannel(th.Context, guestUser, privateChannel, false)
    +	require.Nil(t, appErr)
    +	_, appErr = th.App.AddUserToChannel(th.Context, guestUser, th.BasicChannel, false)
    +	require.Nil(t, appErr)
    +
    +	post1 := th.CreatePost(t, th.BasicChannel)
    +	post2 := th.CreatePost(t, privateChannel)
    +	post3 := th.CreatePost(t, th.BasicChannel)
    +
    +	fileInfo1 := th.CreateFileInfo(t, th.BasicUser.Id, post1.Id, th.BasicChannel.Id)
    +	fileInfo2 := th.CreateFileInfo(t, th.BasicUser.Id, post2.Id, privateChannel.Id)
    +	fileInfo3 := th.CreateFileInfo(t, th.BasicUser.Id, post3.Id, th.BasicChannel.Id)
    +
    +	t.Run("should filter files when user has read_channel_content permission", func(t *testing.T) {
    +		fileList := model.NewFileInfoList()
    +		fileList.FileInfos[fileInfo1.Id] = fileInfo1
    +		fileList.FileInfos[fileInfo2.Id] = fileInfo2
    +		fileList.FileInfos[fileInfo3.Id] = fileInfo3
    +		fileList.Order = []string{fileInfo1.Id, fileInfo2.Id, fileInfo3.Id}
    +
    +		// BasicUser should have access to all files
    +		appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, fileList.FileInfos, 3)
    +		require.Len(t, fileList.Order, 3)
    +	})
    +
    +	t.Run("should filter files when guest has read_channel_content permission", func(t *testing.T) {
    +		fileList := model.NewFileInfoList()
    +		fileList.FileInfos[fileInfo1.Id] = fileInfo1
    +		fileList.FileInfos[fileInfo2.Id] = fileInfo2
    +		fileList.FileInfos[fileInfo3.Id] = fileInfo3
    +		fileList.Order = []string{fileInfo1.Id, fileInfo2.Id, fileInfo3.Id}
    +
    +		appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, guestUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, fileList.FileInfos, 3)
    +		require.Len(t, fileList.Order, 3)
    +	})
    +
    +	t.Run("should filter files when guest does not have read_channel_content permission", func(t *testing.T) {
    +		channelGuestRole, appErr := th.App.GetRoleByName(th.Context, model.ChannelGuestRoleId)
    +		require.Nil(t, appErr)
    +
    +		originalPermissions := make([]string, len(channelGuestRole.Permissions))
    +		copy(originalPermissions, channelGuestRole.Permissions)
    +
    +		newPermissions := []string{}
    +		for _, perm := range channelGuestRole.Permissions {
    +			if perm != model.PermissionReadChannelContent.Id && perm != model.PermissionReadChannel.Id {
    +				newPermissions = append(newPermissions, perm)
    +			}
    +		}
    +
    +		_, appErr = th.App.PatchRole(channelGuestRole, &model.RolePatch{
    +			Permissions: &newPermissions,
    +		})
    +		require.Nil(t, appErr)
    +
    +		defer func() {
    +			_, err := th.App.PatchRole(channelGuestRole, &model.RolePatch{
    +				Permissions: &originalPermissions,
    +			})
    +			require.Nil(t, err)
    +		}()
    +
    +		fileList := model.NewFileInfoList()
    +		fileList.FileInfos[fileInfo1.Id] = fileInfo1
    +		fileList.FileInfos[fileInfo2.Id] = fileInfo2
    +		fileList.FileInfos[fileInfo3.Id] = fileInfo3
    +		fileList.Order = []string{fileInfo1.Id, fileInfo2.Id, fileInfo3.Id}
    +
    +		appErr = th.App.FilterFilesByChannelPermissions(th.Context, fileList, guestUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, fileList.FileInfos, 0)
    +		require.Len(t, fileList.Order, 0)
    +	})
    +
    +	t.Run("should handle empty file list", func(t *testing.T) {
    +		fileList := model.NewFileInfoList()
    +		appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, fileList.FileInfos, 0)
    +		require.Len(t, fileList.Order, 0)
    +	})
    +
    +	t.Run("should handle nil file list", func(t *testing.T) {
    +		appErr := th.App.FilterFilesByChannelPermissions(th.Context, nil, th.BasicUser.Id)
    +		require.Nil(t, appErr)
    +	})
    +
    +	t.Run("should handle files with empty channel IDs", func(t *testing.T) {
    +		fileList := model.NewFileInfoList()
    +		fileWithoutChannel := &model.FileInfo{
    +			Id:        model.NewId(),
    +			ChannelId: "",
    +			Name:      "test.txt",
    +		}
    +		fileList.FileInfos[fileWithoutChannel.Id] = fileWithoutChannel
    +		fileList.Order = []string{fileWithoutChannel.Id}
    +
    +		appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, fileList.FileInfos, 0)
    +		require.Len(t, fileList.Order, 0)
    +	})
    +
    +	t.Run("should handle files from non-existent channels", func(t *testing.T) {
    +		fileList := model.NewFileInfoList()
    +		fileWithInvalidChannel := &model.FileInfo{
    +			Id:        model.NewId(),
    +			ChannelId: model.NewId(),
    +			Name:      "test.txt",
    +		}
    +		fileList.FileInfos[fileWithInvalidChannel.Id] = fileWithInvalidChannel
    +		fileList.Order = []string{fileWithInvalidChannel.Id}
    +
    +		appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, fileList.FileInfos, 0)
    +		require.Len(t, fileList.Order, 0)
    +	})
    +}
    
  • server/channels/app/post.go+61 0 modified
    @@ -15,6 +15,9 @@ import (
     	"sync"
     	"time"
     
    +	"maps"
    +	"slices"
    +
     	agentclient "github.com/mattermost/mattermost-plugin-ai/public/bridgeclient"
     	"github.com/mattermost/mattermost/server/public/model"
     	"github.com/mattermost/mattermost/server/public/plugin"
    @@ -1935,13 +1938,71 @@ func (a *App) SearchPostsForUser(rctx request.CTX, terms string, userID string,
     		return nil, appErr
     	}
     
    +	if appErr := a.FilterPostsByChannelPermissions(rctx, postSearchResults.PostList, userID); appErr != nil {
    +		return nil, appErr
    +	}
    +
     	if appErr := a.filterBurnOnReadPosts(postSearchResults.PostList); appErr != nil {
     		return nil, appErr
     	}
     
     	return postSearchResults, nil
     }
     
    +func (a *App) FilterPostsByChannelPermissions(rctx request.CTX, postList *model.PostList, userID string) *model.AppError {
    +	if postList == nil || postList.Posts == nil || len(postList.Posts) == 0 {
    +		return nil
    +	}
    +
    +	channels := make(map[string]*model.Channel)
    +	for _, post := range postList.Posts {
    +		if post.ChannelId != "" {
    +			channels[post.ChannelId] = nil
    +		}
    +	}
    +
    +	if len(channels) > 0 {
    +		channelIDs := slices.Collect(maps.Keys(channels))
    +		channelList, err := a.GetChannels(rctx, channelIDs)
    +		if err != nil && err.StatusCode != http.StatusNotFound {
    +			return err
    +		}
    +		for _, channel := range channelList {
    +			channels[channel.Id] = channel
    +		}
    +	}
    +
    +	channelReadPermission := make(map[string]bool)
    +	filteredPosts := make(map[string]*model.Post)
    +	filteredOrder := []string{}
    +
    +	for _, postID := range postList.Order {
    +		post, ok := postList.Posts[postID]
    +		if !ok {
    +			continue
    +		}
    +
    +		if _, ok := channelReadPermission[post.ChannelId]; !ok {
    +			channel := channels[post.ChannelId]
    +			allowed := false
    +			if channel != nil {
    +				allowed = a.HasPermissionToReadChannel(rctx, userID, channel)
    +			}
    +			channelReadPermission[post.ChannelId] = allowed
    +		}
    +
    +		if channelReadPermission[post.ChannelId] {
    +			filteredPosts[postID] = post
    +			filteredOrder = append(filteredOrder, postID)
    +		}
    +	}
    +
    +	postList.Posts = filteredPosts
    +	postList.Order = filteredOrder
    +
    +	return nil
    +}
    +
     func (a *App) GetFileInfosForPostWithMigration(rctx request.CTX, postID string, includeDeleted bool) ([]*model.FileInfo, *model.AppError) {
     	pchan := make(chan store.StoreResult[*model.Post], 1)
     	go func() {
    
  • server/channels/app/post_test.go+133 0 modified
    @@ -4539,6 +4539,139 @@ func TestPopulateEditHistoryFileMetadata(t *testing.T) {
     	})
     }
     
    +func TestFilterPostsByChannelPermissions(t *testing.T) {
    +	mainHelper.Parallel(t)
    +	th := Setup(t).InitBasic(t)
    +
    +	th.App.UpdateConfig(func(cfg *model.Config) {
    +		*cfg.GuestAccountsSettings.Enable = true
    +	})
    +
    +	guestUser := th.CreateGuest(t)
    +	_, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guestUser.Id, "")
    +	require.Nil(t, appErr)
    +
    +	privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
    +
    +	_, appErr = th.App.AddUserToChannel(th.Context, guestUser, privateChannel, false)
    +	require.Nil(t, appErr)
    +	_, appErr = th.App.AddUserToChannel(th.Context, guestUser, th.BasicChannel, false)
    +	require.Nil(t, appErr)
    +
    +	post1 := th.CreatePost(t, th.BasicChannel)
    +	post2 := th.CreatePost(t, privateChannel)
    +	post3 := th.CreatePost(t, th.BasicChannel)
    +
    +	t.Run("should filter posts when user has read_channel_content permission", func(t *testing.T) {
    +		postList := model.NewPostList()
    +		postList.Posts[post1.Id] = post1
    +		postList.Posts[post2.Id] = post2
    +		postList.Posts[post3.Id] = post3
    +		postList.Order = []string{post1.Id, post2.Id, post3.Id}
    +
    +		appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, postList.Posts, 3)
    +		require.Len(t, postList.Order, 3)
    +	})
    +
    +	t.Run("should filter posts when guest has read_channel_content permission", func(t *testing.T) {
    +		postList := model.NewPostList()
    +		postList.Posts[post1.Id] = post1
    +		postList.Posts[post2.Id] = post2
    +		postList.Posts[post3.Id] = post3
    +		postList.Order = []string{post1.Id, post2.Id, post3.Id}
    +
    +		appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, guestUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, postList.Posts, 3)
    +		require.Len(t, postList.Order, 3)
    +	})
    +
    +	t.Run("should filter posts when guest does not have read_channel_content permission", func(t *testing.T) {
    +		channelGuestRole, appErr := th.App.GetRoleByName(th.Context, model.ChannelGuestRoleId)
    +		require.Nil(t, appErr)
    +
    +		originalPermissions := make([]string, len(channelGuestRole.Permissions))
    +		copy(originalPermissions, channelGuestRole.Permissions)
    +
    +		newPermissions := []string{}
    +		for _, perm := range channelGuestRole.Permissions {
    +			if perm != model.PermissionReadChannelContent.Id && perm != model.PermissionReadChannel.Id {
    +				newPermissions = append(newPermissions, perm)
    +			}
    +		}
    +
    +		_, appErr = th.App.PatchRole(channelGuestRole, &model.RolePatch{
    +			Permissions: &newPermissions,
    +		})
    +		require.Nil(t, appErr)
    +
    +		defer func() {
    +			_, err := th.App.PatchRole(channelGuestRole, &model.RolePatch{
    +				Permissions: &originalPermissions,
    +			})
    +			require.Nil(t, err)
    +		}()
    +
    +		postList := model.NewPostList()
    +		postList.Posts[post1.Id] = post1
    +		postList.Posts[post2.Id] = post2
    +		postList.Posts[post3.Id] = post3
    +		postList.Order = []string{post1.Id, post2.Id, post3.Id}
    +
    +		appErr = th.App.FilterPostsByChannelPermissions(th.Context, postList, guestUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, postList.Posts, 0)
    +		require.Len(t, postList.Order, 0)
    +	})
    +
    +	t.Run("should handle empty post list", func(t *testing.T) {
    +		postList := model.NewPostList()
    +		appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, postList.Posts, 0)
    +		require.Len(t, postList.Order, 0)
    +	})
    +
    +	t.Run("should handle nil post list", func(t *testing.T) {
    +		appErr := th.App.FilterPostsByChannelPermissions(th.Context, nil, th.BasicUser.Id)
    +		require.Nil(t, appErr)
    +	})
    +
    +	t.Run("should handle posts with empty channel IDs", func(t *testing.T) {
    +		postList := model.NewPostList()
    +		postWithoutChannel := &model.Post{
    +			Id:        model.NewId(),
    +			ChannelId: "",
    +			Message:   "test",
    +		}
    +		postList.Posts[postWithoutChannel.Id] = postWithoutChannel
    +		postList.Order = []string{postWithoutChannel.Id}
    +
    +		appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, postList.Posts, 0)
    +		require.Len(t, postList.Order, 0)
    +	})
    +
    +	t.Run("should handle posts from non-existent channels", func(t *testing.T) {
    +		postList := model.NewPostList()
    +		postWithInvalidChannel := &model.Post{
    +			Id:        model.NewId(),
    +			ChannelId: model.NewId(),
    +			Message:   "test",
    +		}
    +		postList.Posts[postWithInvalidChannel.Id] = postWithInvalidChannel
    +		postList.Order = []string{postWithInvalidChannel.Id}
    +
    +		appErr := th.App.FilterPostsByChannelPermissions(th.Context, postList, th.BasicUser.Id)
    +		require.Nil(t, appErr)
    +		require.Len(t, postList.Posts, 0)
    +		require.Len(t, postList.Order, 0)
    +	})
    +}
    +
     func TestRevealPost(t *testing.T) {
     	os.Setenv("MM_FEATUREFLAGS_BURNONREAD", "true")
     	t.Cleanup(func() {
    

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.