VYPR
Low severityNVD Advisory· Published Sep 19, 2025· Updated Sep 19, 2025

IDOR in board file download allows any user to download any file by UUID

CVE-2025-9081

Description

Mattermost versions 10.5.x <= 10.5.8, 9.11.x <= 9.11.17 fail to properly validate access controls which allows any authenticated user to download sensitive files via board file download endpoint using UUID enumeration

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost-plugin-boardsGo
< 0.0.0-20250716054606-3f3e3becfe1d0.0.0-20250716054606-3f3e3becfe1d
github.com/mattermost/mattermost/server/v8Go
< 8.0.0-20250721095935-11c36f4d1e448.0.0-20250721095935-11c36f4d1e44
github.com/mattermost/mattermost-serverGo
>= 10.5.0-rc1, < 10.5.910.5.9
github.com/mattermost/mattermost-serverGo
>= 9.11.0-rc1, < 9.11.189.11.18

Affected products

1

Patches

1
3f3e3becfe1d

Added file ownership validation to file access endpoints (#114)

3 files changed · +181 0
  • server/api/files.go+7 0 modified
    @@ -303,6 +303,12 @@ func (a *API) getFileInfo(w http.ResponseWriter, r *http.Request) {
     	auditRec.AddMeta("teamID", teamID)
     	auditRec.AddMeta("filename", filename)
     
    +	// Validate that the file belongs to the specified board and team
    +	if err := a.app.ValidateFileOwnership(teamID, boardID, filename); err != nil {
    +		a.errorResponse(w, r, model.NewErrPermission("access denied to file"))
    +		return
    +	}
    +
     	fileInfo, err := a.app.GetFileInfo(filename)
     	if err != nil && !model.IsErrNotFound(err) {
     		a.errorResponse(w, r, err)
    @@ -316,6 +322,7 @@ func (a *API) getFileInfo(w http.ResponseWriter, r *http.Request) {
     	}
     
     	jsonBytesResponse(w, http.StatusOK, data)
    +	auditRec.Success()
     }
     
     func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
    
  • server/app/files.go+69 0 modified
    @@ -23,6 +23,7 @@ const emptyString = "empty"
     
     var errEmptyFilename = errors.New("IsFileArchived: empty filename not allowed")
     var ErrFileNotFound = errors.New("file not found")
    +var ErrFileNotReferencedByBoard = errors.New("file not referenced by board")
     
     func (a *App) SaveFile(reader io.Reader, teamID, boardID, filename string, asTemplate bool) (string, error) {
     	// NOTE: File extension includes the dot
    @@ -86,7 +87,75 @@ func (a *App) GetFileInfo(filename string) (*mm_model.FileInfo, error) {
     	return fileInfo, nil
     }
     
    +// ValidateFileOwnership checks if a file belongs to the specified board and team.
    +func (a *App) ValidateFileOwnership(teamID, boardID, filename string) error {
    +	fileInfo, err := a.GetFileInfo(filename)
    +	if err != nil {
    +		return err
    +	}
    +	if fileInfo != nil && fileInfo.Path != "" && fileInfo.Path != emptyString {
    +		expectedPath := filepath.Join(teamID, boardID, filename)
    +		if fileInfo.Path == expectedPath {
    +			return nil
    +		}
    +		if err := a.validateFileReferencedByBoard(boardID, filename); err != nil {
    +			return model.NewErrPermission("file does not belong to the specified board")
    +		}
    +	} else {
    +		if err := a.validateFileReferencedByBoard(boardID, filename); err != nil {
    +			return model.NewErrPermission("file does not belong to the specified board")
    +		}
    +	}
    +	return nil
    +}
    +
    +// validateFileReferencedByBoard checks if a file is referenced by blocks in the specified board.
    +// Files use different storage patterns (teamID/boardID/filename for templates, boards/YYYYMMDD/filename for regular files).
    +// Path mismatches don't indicate malicious files, just different storage patterns.
    +func (a *App) validateFileReferencedByBoard(boardID, filename string) error {
    +	imageBlocks, err := a.store.GetBlocksWithType(boardID, model.TypeImage)
    +	if err != nil {
    +		return err
    +	}
    +
    +	attachmentBlocks, err := a.store.GetBlocksWithType(boardID, model.TypeAttachment)
    +	if err != nil {
    +		return err
    +	}
    +
    +	// Check image blocks
    +	for _, block := range imageBlocks {
    +		if fileID, ok := block.Fields[model.BlockFieldFileId].(string); ok && fileID == filename {
    +			return nil
    +		}
    +		if attachmentID, ok := block.Fields[model.BlockFieldAttachmentId].(string); ok && attachmentID == filename {
    +			return nil
    +		}
    +	}
    +
    +	// Check attachment blocks
    +	for _, block := range attachmentBlocks {
    +		if fileID, ok := block.Fields[model.BlockFieldFileId].(string); ok && fileID == filename {
    +			return nil
    +		}
    +		if attachmentID, ok := block.Fields[model.BlockFieldAttachmentId].(string); ok && attachmentID == filename {
    +			return nil
    +		}
    +	}
    +
    +	return fmt.Errorf("%w: file %s is not referenced by any block in board %s", ErrFileNotReferencedByBoard, filename, boardID)
    +}
    +
     func (a *App) GetFile(teamID, boardID, fileName string) (*mm_model.FileInfo, filestore.ReadCloseSeeker, error) {
    +	if err := a.ValidateFileOwnership(teamID, boardID, fileName); err != nil {
    +		a.logger.Error("GetFile: File ownership validation failed",
    +			mlog.String("Team", teamID),
    +			mlog.String("board", boardID),
    +			mlog.String("filename", fileName),
    +			mlog.Err(err))
    +		return nil, nil, err
    +	}
    +
     	fileInfo, filePath, err := a.GetFilePath(teamID, boardID, fileName)
     	if err != nil {
     		a.logger.Error("GetFile: Failed to GetFilePath.", mlog.String("Team", teamID), mlog.String("board", boardID), mlog.String("filename", fileName), mlog.Err(err))
    
  • server/app/files_test.go+105 0 modified
    @@ -929,6 +929,111 @@ func TestUserCreatedTemplateFilePathValidation(t *testing.T) {
     	})
     }
     
    +func TestValidateFileOwnership(t *testing.T) {
    +	th, _ := SetupTestHelper(t)
    +
    +	validTeamID := "validteamid1234567890123456"
    +	validBoardID := "bvalidboard1234567890123456"
    +	otherBoardID := "botherboard1234567890123456"
    +	filename := "7validfile1234567890123456.txt"
    +
    +	t.Run("Should allow access to file that belongs to the board", func(t *testing.T) {
    +		// Mock file info with path matching the board
    +		fileInfo := &mm_model.FileInfo{
    +			Id:   "validfile1234567890123456",
    +			Path: filepath.Join(validTeamID, validBoardID, filename),
    +		}
    +		th.Store.EXPECT().GetFileInfo("validfile1234567890123456").Return(fileInfo, nil)
    +
    +		err := th.App.ValidateFileOwnership(validTeamID, validBoardID, filename)
    +		assert.NoError(t, err)
    +	})
    +
    +	t.Run("Should allow access to file with base path that is referenced by board", func(t *testing.T) {
    +		// Mock file info with base path (newer storage format)
    +		fileInfo := &mm_model.FileInfo{
    +			Id:   "validfile1234567890123456",
    +			Path: filepath.Join(utils.GetBaseFilePath(), filename),
    +		}
    +		th.Store.EXPECT().GetFileInfo("validfile1234567890123456").Return(fileInfo, nil)
    +
    +		// Mock block that references the file
    +		block := &model.Block{
    +			ID:      "blockid1234567890123456789",
    +			BoardID: validBoardID,
    +			Type:    model.TypeImage,
    +			Fields:  map[string]interface{}{model.BlockFieldFileId: filename},
    +		}
    +		th.Store.EXPECT().GetBlocksForBoard(validBoardID).Return([]*model.Block{block}, nil)
    +
    +		err := th.App.ValidateFileOwnership(validTeamID, validBoardID, filename)
    +		assert.NoError(t, err)
    +	})
    +
    +	t.Run("Should deny access to file that belongs to different board", func(t *testing.T) {
    +		// Mock file info with path from different board
    +		fileInfo := &mm_model.FileInfo{
    +			Id:   "validfile1234567890123456",
    +			Path: filepath.Join(validTeamID, otherBoardID, filename),
    +		}
    +		th.Store.EXPECT().GetFileInfo("validfile1234567890123456").Return(fileInfo, nil)
    +
    +		// Mock empty blocks for the requested board (file not referenced)
    +		th.Store.EXPECT().GetBlocksForBoard(validBoardID).Return([]*model.Block{}, nil)
    +
    +		err := th.App.ValidateFileOwnership(validTeamID, validBoardID, filename)
    +		assert.Error(t, err)
    +		assert.Contains(t, err.Error(), "file does not belong to the specified board")
    +	})
    +
    +	t.Run("Should deny access to file that is not referenced by any block in the board", func(t *testing.T) {
    +		fileInfo := &mm_model.FileInfo{
    +			Id:   "validfile1234567890123456",
    +			Path: filepath.Join(utils.GetBaseFilePath(), filename),
    +		}
    +		th.Store.EXPECT().GetFileInfo("validfile1234567890123456").Return(fileInfo, nil)
    +
    +		block := &model.Block{
    +			ID:      "blockid1234567890123456789",
    +			BoardID: validBoardID,
    +			Type:    model.TypeImage,
    +			Fields:  map[string]interface{}{model.BlockFieldFileId: "different_file.txt"},
    +		}
    +		th.Store.EXPECT().GetBlocksForBoard(validBoardID).Return([]*model.Block{block}, nil)
    +
    +		err := th.App.ValidateFileOwnership(validTeamID, validBoardID, filename)
    +		assert.Error(t, err)
    +		assert.Contains(t, err.Error(), "file does not belong to the specified board")
    +	})
    +
    +	t.Run("Should allow access to file referenced by attachment field", func(t *testing.T) {
    +		fileInfo := &mm_model.FileInfo{
    +			Id:   "validfile1234567890123456",
    +			Path: filepath.Join(utils.GetBaseFilePath(), filename),
    +		}
    +		th.Store.EXPECT().GetFileInfo("validfile1234567890123456").Return(fileInfo, nil)
    +
    +		block := &model.Block{
    +			ID:      "blockid1234567890123456789",
    +			BoardID: validBoardID,
    +			Type:    model.TypeAttachment,
    +			Fields:  map[string]interface{}{model.BlockFieldAttachmentId: filename},
    +		}
    +		th.Store.EXPECT().GetBlocksForBoard(validBoardID).Return([]*model.Block{block}, nil)
    +
    +		err := th.App.ValidateFileOwnership(validTeamID, validBoardID, filename)
    +		assert.NoError(t, err)
    +	})
    +
    +	t.Run("Should handle file info not found", func(t *testing.T) {
    +		th.Store.EXPECT().GetFileInfo("validfile1234567890123456").Return(nil, model.NewErrNotFound("file not found"))
    +
    +		err := th.App.ValidateFileOwnership(validTeamID, validBoardID, filename)
    +		assert.Error(t, err)
    +		assert.Contains(t, err.Error(), "file not found")
    +	})
    +}
    +
     func TestGetFilePathWithGlobalTeamID(t *testing.T) {
     	th, _ := SetupTestHelper(t)
     
    

Vulnerability mechanics

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

References

6

News mentions

0

No linked articles in our index yet.