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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/mattermost/mattermost/server/v8Go | < 8.0.0-20260107142155-0481bd1fb045 | 8.0.0-20260107142155-0481bd1fb045 |
github.com/mattermost/mattermost-serverGo | < 5.3.2-0.20260107142155-0481bd1fb045 | 5.3.2-0.20260107142155-0481bd1fb045 |
github.com/mattermost/mattermost-serverGo | >= 10.11.0-rc1, < 10.11.11 | 10.11.11 |
github.com/mattermost/mattermost-serverGo | >= 11.2.0-rc1, < 11.2.3 | 11.2.3 |
github.com/mattermost/mattermost-serverGo | >= 11.3.0-rc1, < 11.3.1 | 11.3.1 |
Affected products
1- Range: 11.3.0
Patches
10481bd1fb045Fliter post in search api with no read content channel permission (#34620)
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- github.com/advisories/GHSA-cwfj-642j-gfh4ghsaADVISORY
- mattermost.com/security-updatesghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-24692ghsaADVISORY
- github.com/mattermost/mattermost/commit/0481bd1fb04584db97eca45fd58ebd06c8200df4ghsaWEB
News mentions
0No linked articles in our index yet.