Repository: mattermost/mattermost
Pull request #31201: [MM-64402] Improve validation of imported attachments
Author: login: streamer45
Labels: name: 4: Reviews Complete
description: All reviewers have approved the pull request
name: Changelog/Not Needed
description: Does not require a changelog entry
name: Docs/Not Needed
description: Does not require documentation
name: CherryPick/Done
description: Successfully cherry-picked to the quality or patch release tracked in the milestone
name: QA - Review Done
description: QA review is completed but other reviews are outstanding (exception to usual process)
name: 3: Security Review
description: Review requested from Security Team
name: release-note-none
description: Denotes a PR that doesn't merit a release note.
name: E2E/Overridden
description: Override all mandatory E2E commit statuses in failing state
Created: 2025-05-27T15:38:36Z | Merged: 2025-05-29T05:44:50Z

#### Summary

PR implements some extra validation on the paths used to import file attachments.

We implement a `ValidateAttachmentImportData` util function called by validators on parent structures (namely `PostImportData`, `ReplyImportData`, and `DirectPostImportData`), which returns an error during initial validation in case of invalid paths. At the same time, we add some additional protection to `processAttachmentPaths`, which is called as part of the bulk import logic.

One thing I am omitting for now is any validation on the base path used, noticing that [we default](https://github.com/mattermost/mattermost/blob/62bd9d917d3906a02a2b670254fbfb4bc672a2ca/server/channels/jobs/import_process/worker.go#L128) to the `model.ExportDataDir` constant when running import jobs.

Changes should be fully backward compatible. We are planning some manual QA to ensure that functionality has remained the same.

#### Ticket Link

https://mattermost.atlassian.net/browse/MM-64402

#### Release Note

```release-note
NONE
```



Diff:
diff --git a/server/channels/app/import.go b/server/channels/app/import.go
index fc41fc890e1..62ade80f9f9 100644
--- a/server/channels/app/import.go
+++ b/server/channels/app/import.go
@@ -7,6 +7,7 @@ import (
 	"archive/zip"
 	"bufio"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -45,20 +46,32 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var errs []error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				errs = append(errs, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
+					errs = append(errs, fmt.Errorf("attachment %q not found in map", originalPath))
+					continue
 				}
 			}
 		}
 	}
 
-	return nil
+	return errors.Join(errs...)
 }
 
 func processAttachments(c request.CTX, line *imports.LineImportData, basePath string, filesMap map[string]*zip.File) error {
@@ -88,7 +101,11 @@ func processAttachments(c request.CTX, line *imports.LineImportData, basePath st
 		}
 	case "user":
 		if line.User.ProfileImage != nil {
-			path := filepath.Join(basePath, *line.User.ProfileImage)
+			path, valid := imports.ValidateAttachmentPathForImport(*line.User.ProfileImage, basePath)
+			if !valid {
+				return fmt.Errorf("invalid profile image path %q", *line.User.ProfileImage)
+			}
+
 			*line.User.ProfileImage = path
 			if len(filesMap) > 0 {
 				if line.User.ProfileImageData, ok = filesMap[path]; !ok {
@@ -98,7 +115,11 @@ func processAttachments(c request.CTX, line *imports.LineImportData, basePath st
 		}
 	case "bot":
 		if line.Bot.ProfileImage != nil {
-			path := filepath.Join(basePath, *line.Bot.ProfileImage)
+			path, valid := imports.ValidateAttachmentPathForImport(*line.Bot.ProfileImage, basePath)
+			if !valid {
+				return fmt.Errorf("invalid bot profile image path %q", *line.Bot.ProfileImage)
+			}
+
 			*line.Bot.ProfileImage = path
 			if len(filesMap) > 0 {
 				if line.Bot.ProfileImageData, ok = filesMap[path]; !ok {
@@ -108,7 +129,11 @@ func processAttachments(c request.CTX, line *imports.LineImportData, basePath st
 		}
 	case "emoji":
 		if line.Emoji.Image != nil {
-			path := filepath.Join(basePath, *line.Emoji.Image)
+			path, valid := imports.ValidateAttachmentPathForImport(*line.Emoji.Image, basePath)
+			if !valid {
+				return fmt.Errorf("invalid emoji image path %q", *line.Emoji.Image)
+			}
+
 			*line.Emoji.Image = path
 			if len(filesMap) > 0 {
 				if line.Emoji.Data, ok = filesMap[path]; !ok {
diff --git a/server/channels/app/import_functions.go b/server/channels/app/import_functions.go
index 8e3ced2b9b4..55fcc00043b 100644
--- a/server/channels/app/import_functions.go
+++ b/server/channels/app/import_functions.go
@@ -193,7 +193,6 @@ func (a *App) importTeam(rctx request.CTX, data *imports.TeamImportData, dryRun
 
 	var team *model.Team
 	team, err := a.Srv().Store().Team().GetByName(teamName)
-
 	if err != nil {
 		team = &model.Team{
 			Name: teamName,
@@ -2063,6 +2062,7 @@ func (a *App) updateFileInfoWithPostId(rctx request.CTX, post *model.Post) {
 		}
 	}
 }
+
 func (a *App) importDirectChannel(rctx request.CTX, data *imports.DirectChannelImportData, dryRun bool) *model.AppError {
 	var err *model.AppError
 	if err = imports.ValidateDirectChannelImportData(data); err != nil {
@@ -2117,7 +2117,7 @@ func (a *App) importDirectChannel(rctx request.CTX, data *imports.DirectChannelI
 		return model.NewAppError("BulkImport", "app.import.import_direct_channel.get_channel_members.error", nil, "", http.StatusBadRequest).Wrap(err)
 	}
 
-	var ems = make([]model.ChannelMember, 0, totalMembers)
+	ems := make([]model.ChannelMember, 0, totalMembers)
 	var page int
 
 	for int64(len(ems)) < totalMembers {
diff --git a/server/channels/app/import_test.go b/server/channels/app/import_test.go
index db7b3afe8ea..84b4b0e6a46 100644
--- a/server/channels/app/import_test.go
+++ b/server/channels/app/import_test.go
@@ -157,7 +157,7 @@ func TestImportBulkImport(t *testing.T) {
 	username3 := model.NewUsername()
 	emojiName := model.NewId()
 	testsDir, _ := fileutils.FindDir("tests")
-	testImage := filepath.Join(testsDir, "test.png")
+	testImage := "test.png"
 	teamTheme1 := `{\"awayIndicator\":\"#DBBD4E\",\"buttonBg\":\"#23A1FF\",\"buttonColor\":\"#FFFFFF\",\"centerChannelBg\":\"#ffffff\",\"centerChannelColor\":\"#333333\",\"codeTheme\":\"github\",\"image\":\"/static/files/a4a388b38b32678e83823ef1b3e17766.png\",\"linkColor\":\"#2389d7\",\"mentionBg\":\"#2389d7\",\"mentionColor\":\"#ffffff\",\"mentionHighlightBg\":\"#fff2bb\",\"mentionHighlightLink\":\"#2f81b7\",\"newMessageSeparator\":\"#FF8800\",\"onlineIndicator\":\"#7DBE00\",\"sidebarBg\":\"#fafafa\",\"sidebarHeaderBg\":\"#3481B9\",\"sidebarHeaderTextColor\":\"#ffffff\",\"sidebarText\":\"#333333\",\"sidebarTextActiveBorder\":\"#378FD2\",\"sidebarTextActiveColor\":\"#111111\",\"sidebarTextHoverBg\":\"#e6f2fa\",\"sidebarUnreadText\":\"#333333\",\"type\":\"Mattermost\"}`
 	teamTheme2 := `{\"awayIndicator\":\"#DBBD4E\",\"buttonBg\":\"#23A100\",\"buttonColor\":\"#EEEEEE\",\"centerChannelBg\":\"#ffffff\",\"centerChannelColor\":\"#333333\",\"codeTheme\":\"github\",\"image\":\"/static/files/a4a388b38b32678e83823ef1b3e17766.png\",\"linkColor\":\"#2389d7\",\"mentionBg\":\"#2389d7\",\"mentionColor\":\"#ffffff\",\"mentionHighlightBg\":\"#fff2bb\",\"mentionHighlightLink\":\"#2f81b7\",\"newMessageSeparator\":\"#FF8800\",\"onlineIndicator\":\"#7DBE00\",\"sidebarBg\":\"#fafafa\",\"sidebarHeaderBg\":\"#3481B9\",\"sidebarHeaderTextColor\":\"#ffffff\",\"sidebarText\":\"#333333\",\"sidebarTextActiveBorder\":\"#378FD2\",\"sidebarTextActiveColor\":\"#222222\",\"sidebarTextHoverBg\":\"#e6f2fa\",\"sidebarUnreadText\":\"#444444\",\"type\":\"Mattermost\"}`
 
@@ -178,13 +178,13 @@ func TestImportBulkImport(t *testing.T) {
 {"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `", "` + username3 + `"], "user": "` + username + `", "message": "Hello Group Channel", "create_at": 123456789015}}
 {"type": "emoji", "emoji": {"name": "` + emojiName + `", "image": "` + testImage + `"}}`
 
-	line, err := th.App.BulkImport(th.Context, strings.NewReader(data1), nil, false, 2)
+	line, err := th.App.BulkImportWithPath(th.Context, strings.NewReader(data1), nil, false, false, 2, testsDir)
 	require.Nil(t, err, "BulkImport should have succeeded")
 	require.Equal(t, 0, line, "BulkImport line should be 0")
 
 	// Run bulk import using a string that contains a line with invalid json.
 	data2 := `{"type": "version", "version": 1`
-	line, err = th.App.BulkImport(th.Context, strings.NewReader(data2), nil, false, 2)
+	line, err = th.App.BulkImportWithPath(th.Context, strings.NewReader(data2), nil, false, false, 2, testsDir)
 	require.NotNil(t, err, "Should have failed due to invalid JSON on line 1.")
 	require.Equal(t, 1, line, "Should have failed due to invalid JSON on line 1.")
 
@@ -193,7 +193,7 @@ func TestImportBulkImport(t *testing.T) {
 {"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}}
 {"type": "user", "user": {"username": "kufjgnkxkrhhfgbrip6qxkfsaa", "email": "kufjgnkxkrhhfgbrip6qxkfsaa@example.com"}}
 {"type": "user", "user": {"username": "bwshaim6qnc2ne7oqkd5b2s2rq", "email": "bwshaim6qnc2ne7oqkd5b2s2rq@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}}`
-	line, err = th.App.BulkImport(th.Context, strings.NewReader(data3), nil, false, 2)
+	line, err = th.App.BulkImportWithPath(th.Context, strings.NewReader(data3), nil, false, false, 2, testsDir)
 	require.NotNil(t, err, "Should have failed due to missing version line on line 1.")
 	require.Equal(t, 1, line, "Should have failed due to missing version line on line 1.")
 
@@ -231,6 +231,54 @@ func TestImportBulkImport(t *testing.T) {
 		require.Nil(t, err, "BulkImport should have succeeded")
 		require.Equal(t, 0, line, "BulkImport line should be 0")
 	})
+
+	t.Run("Invalid post attachment path", func(t *testing.T) {
+		data7 := `{"type": "version", "version": 1}
+{"type": "team", "team": {"type": "O", "display_name": "lskmw2d7a5ao7ppwqh5ljchvr4", "name": "` + teamName + `"}}
+{"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}}
+{"type": "user", "user": {"username": "` + username + `", "email": "` + username + `@example.com", "teams": [{"name": "` + teamName + `","theme": "` + teamTheme1 + `", "channels": [{"name": "` + channelName + `"}]}]}}
+{"type": "user", "user": {"username": "` + username2 + `", "email": "` + username2 + `@example.com", "teams": [{"name": "` + teamName + `","theme": "` + teamTheme2 + `", "channels": [{"name": "` + channelName + `"}]}]}}
+{"type": "user", "user": {"username": "` + username3 + `", "email": "` + username3 + `@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}], "delete_at": 123456789016}]}}
+{"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username + `", "message": "Hello World", "create_at": 123456789012, "attachments":[{"path": "test.png"}]}}
+{"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username3 + `", "message": "Hey Everyone!", "create_at": 123456789013, "attachments":[{"path": "../test.png"}]}}`
+
+		// Import should not fail for a single invalid attachment path.
+		line, err := th.App.BulkImportWithPath(th.Context, strings.NewReader(data7), nil, false, false, 2, testsDir)
+		require.Nil(t, err, "BulkImport should have succeeded")
+		require.Equal(t, 0, line, "BulkImport line should be 0")
+	})
+
+	t.Run("Invalid reply attachment path", func(t *testing.T) {
+		data8 := `{"type": "version", "version": 1}
+{"type": "team", "team": {"type": "O", "display_name": "lskmw2d7a5ao7ppwqh5ljchvr4", "name": "` + teamName + `"}}
+{"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}}
+{"type": "user", "user": {"username": "` + username + `", "email": "` + username + `@example.com", "teams": [{"name": "` + teamName + `","theme": "` + teamTheme1 + `", "channels": [{"name": "` + channelName + `"}]}]}}
+{"type": "user", "user": {"username": "` + username2 + `", "email": "` + username2 + `@example.com", "teams": [{"name": "` + teamName + `","theme": "` + teamTheme2 + `", "channels": [{"name": "` + channelName + `"}]}]}}
+{"type": "user", "user": {"username": "` + username3 + `", "email": "` + username3 + `@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}], "delete_at": 123456789016}]}}
+{"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username + `", "message": "Hello World", "create_at": 123456789012, "attachments":[{"path": "test.png"}]}}
+{"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username3 + `", "message": "Hey Everyone!", "create_at": 123456789013, "replies": [{"create_at": 123456789015, "user": "` + username + `", "message": "reply", "attachments":[{"path": "../test.png"}]}]}}`
+
+		// Import should not fail for a single invalid attachment path.
+		line, err := th.App.BulkImportWithPath(th.Context, strings.NewReader(data8), nil, false, false, 2, testsDir)
+		require.Nil(t, err, "BulkImport should have succeeded")
+		require.Equal(t, 0, line, "BulkImport line should be 0")
+	})
+
+	t.Run("Invalid direct post attachment path", func(t *testing.T) {
+		data9 := `{"type": "version", "version": 1}
+{"type": "team", "team": {"type": "O", "display_name": "lskmw2d7a5ao7ppwqh5ljchvr4", "name": "` + teamName + `"}}
+{"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}}
+{"type": "user", "user": {"username": "` + username + `", "email": "` + username + `@example.com", "teams": [{"name": "` + teamName + `","theme": "` + teamTheme1 + `", "channels": [{"name": "` + channelName + `"}]}]}}
+{"type": "user", "user": {"username": "` + username2 + `", "email": "` + username2 + `@example.com", "teams": [{"name": "` + teamName + `","theme": "` + teamTheme2 + `", "channels": [{"name": "` + channelName + `"}]}]}}
+{"type": "user", "user": {"username": "` + username3 + `", "email": "` + username3 + `@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}], "delete_at": 123456789016}]}}
+{"type": "direct_channel", "direct_channel": {"members": ["` + username + `", "` + username + `"]}}
+{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username + `"], "user": "` + username + `", "message": "Hello Direct Channel to myself", "create_at": 123456789014, "attachments":[{"path": "../test.png"}]}}`
+
+		// Import should not fail for a single invalid attachment path.
+		line, err := th.App.BulkImportWithPath(th.Context, strings.NewReader(data9), nil, false, false, 2, testsDir)
+		require.Nil(t, err, "BulkImport should have succeeded")
+		require.Equal(t, 0, line, "BulkImport line should be 0")
+	})
 }
 
 func TestImportProcessImportDataFileVersionLine(t *testing.T) {
@@ -275,6 +323,133 @@ func AssertFileIdsInPost(files []*model.FileInfo, th *TestHelper, t *testing.T)
 	}
 }
 
+func TestProcessAttachmentPaths(t *testing.T) {
+	c := request.TestContext(t)
+
+	t.Run("nil attachments", func(t *testing.T) {
+		err := processAttachmentPaths(c, nil, "", nil)
+		require.NoError(t, err)
+	})
+
+	t.Run("missing file in map", func(t *testing.T) {
+		attachments := &[]imports.AttachmentImportData{
+			{
+				Path: model.NewPointer("file.jpg"),
+			},
+		}
+
+		filesMap := map[string]*zip.File{
+			"./import/other-file.jpg": nil,
+		}
+
+		err := processAttachmentPaths(c, attachments, "", filesMap)
+		require.Error(t, err)
+		require.EqualError(t, err, "attachment \"file.jpg\" not found in map")
+	})
+
+	t.Run("valid paths", func(t *testing.T) {
+		attachments := &[]imports.AttachmentImportData{
+			{
+				Path: model.NewPointer("file.jpg"),
+			},
+			{
+				Path: model.NewPointer("somedir/file.jpg"),
+			},
+			{
+				Path: model.NewPointer("./someotherdir/file.jpg"),
+			},
+		}
+
+		expected := &[]imports.AttachmentImportData{
+			{
+				Path: model.NewPointer("data/file.jpg"),
+			},
+			{
+				Path: model.NewPointer("data/somedir/file.jpg"),
+			},
+			{
+				Path: model.NewPointer("data/someotherdir/file.jpg"),
+			},
+		}
+
+		err := processAttachmentPaths(c, attachments, model.ExportDataDir, nil)
+		require.NoError(t, err)
+		require.Equal(t, expected, attachments)
+	})
+
+	t.Run("uncleaned paths", func(t *testing.T) {
+		attachments := &[]imports.AttachmentImportData{
+			{
+				Path: model.NewPointer("../dir/invalid.txt"),
+			},
+			{
+				Path: model.NewPointer("somedir/./normal-file.jpg"),
+			},
+		}
+
+		expected := &[]imports.AttachmentImportData{
+			{
+				Path: model.NewPointer("/path/to/import/dir/invalid.txt"),
+			},
+			{
+				Path: model.NewPointer("/path/to/import/dir/somedir/normal-file.jpg"),
+			},
+		}
+
+		err := processAttachmentPaths(c, attachments, "/path/to/import/dir", nil)
+		require.NoError(t, err)
+		require.Equal(t, expected, attachments)
+	})
+
+	t.Run("paths outside base path", func(t *testing.T) {
+		attachments := &[]imports.AttachmentImportData{
+			{
+				Path: model.NewPointer("../../invalid.txt"),
+			},
+			{
+				Path: model.NewPointer("../../../invalid.txt"),
+			},
+		}
+
+		expected := &[]imports.AttachmentImportData{
+			{
+				Path: model.NewPointer(""),
+			},
+			{
+				Path: model.NewPointer(""),
+			},
+		}
+
+		err := processAttachmentPaths(c, attachments, "data", nil)
+		require.EqualError(t, err, "invalid attachment path \"../../invalid.txt\"\ninvalid attachment path \"../../../invalid.txt\"")
+		require.Equal(t, expected, attachments)
+	})
+
+	t.Run("mix of valid and invalid paths", func(t *testing.T) {
+		attachments := &[]imports.AttachmentImportData{
+			{
+				Path: model.NewPointer("../../invalid.txt"),
+			},
+			{
+				Path: model.NewPointer("valid/path/to/file"),
+			},
+		}
+
+		expected := &[]imports.AttachmentImportData{
+			{
+				Path: model.NewPointer(""),
+			},
+			{
+				Path: model.NewPointer("data/valid/path/to/file"),
+			},
+		}
+
+		err := processAttachmentPaths(c, attachments, "data", nil)
+		require.EqualError(t, err, "invalid attachment path \"../../invalid.txt\"")
+		require.Equal(t, expected, attachments)
+	})
+}
+
 func TestProcessAttachments(t *testing.T) {
 	c := request.TestContext(t)
 
@@ -340,35 +515,35 @@ func TestProcessAttachments(t *testing.T) {
 	t.Run("valid path", func(t *testing.T) {
 		expected := &[]imports.AttachmentImportData{
 			{
-				Path: model.NewPointer("/tmp/file.jpg"),
+				Path: model.NewPointer("tmp/file.jpg"),
 			},
 			{
-				Path: model.NewPointer("/tmp/somedir/file.jpg"),
+				Path: model.NewPointer("tmp/somedir/file.jpg"),
 			},
 		}
 
 		t.Run("post attachments", func(t *testing.T) {
-			err := processAttachments(c, &line, "/tmp", nil)
+			err := processAttachments(c, &line, "tmp", nil)
 			require.NoError(t, err)
 			require.Equal(t, expected, line.Post.Attachments)
 		})
 
 		t.Run("direct post attachments", func(t *testing.T) {
-			err := processAttachments(c, &line2, "/tmp", nil)
+			err := processAttachments(c, &line2, "tmp", nil)
 			require.NoError(t, err)
 			require.Equal(t, expected, line2.DirectPost.Attachments)
 		})
 
 		t.Run("profile image", func(t *testing.T) {
-			expected := "/tmp/profile.jpg"
-			err := processAttachments(c, &userLine, "/tmp", nil)
+			expected := "tmp/profile.jpg"
+			err := processAttachments(c, &userLine, "tmp", nil)
 			require.NoError(t, err)
 			require.Equal(t, expected, *userLine.User.ProfileImage)
 		})
 
 		t.Run("emoji", func(t *testing.T) {
-			expected := "/tmp/emoji.png"
-			err := processAttachments(c, &emojiLine, "/tmp", nil)
+			expected := "tmp/emoji.png"
+			err := processAttachments(c, &emojiLine, "tmp", nil)
 			require.NoError(t, err)
 			require.Equal(t, expected, *emojiLine.Emoji.Image)
 		})
@@ -377,24 +552,24 @@ func TestProcessAttachments(t *testing.T) {
 	t.Run("with filesMap", func(t *testing.T) {
 		t.Run("post attachments", func(t *testing.T) {
 			filesMap := map[string]*zip.File{
-				"/tmp/file.jpg": nil,
+				"tmp/file.jpg": nil,
 			}
 			err := processAttachments(c, &line, "", filesMap)
 			require.Error(t, err)
 
-			filesMap["/tmp/somedir/file.jpg"] = nil
+			filesMap["tmp/somedir/file.jpg"] = nil
 			err = processAttachments(c, &line, "", filesMap)
 			require.NoError(t, err)
 		})
 
 		t.Run("direct post attachments", func(t *testing.T) {
 			filesMap := map[string]*zip.File{
-				"/tmp/file.jpg": nil,
+				"tmp/file.jpg": nil,
 			}
 			err := processAttachments(c, &line2, "", filesMap)
 			require.Error(t, err)
 
-			filesMap["/tmp/somedir/file.jpg"] = nil
+			filesMap["tmp/somedir/file.jpg"] = nil
 			err = processAttachments(c, &line2, "", filesMap)
 			require.NoError(t, err)
 		})
@@ -406,7 +581,7 @@ func TestProcessAttachments(t *testing.T) {
 			err := processAttachments(c, &userLine, "", filesMap)
 			require.Error(t, err)
 
-			filesMap["/tmp/profile.jpg"] = nil
+			filesMap["tmp/profile.jpg"] = nil
 			err = processAttachments(c, &userLine, "", filesMap)
 			require.NoError(t, err)
 		})
@@ -418,7 +593,7 @@ func TestProcessAttachments(t *testing.T) {
 			err := processAttachments(c, &emojiLine, "", filesMap)
 			require.Error(t, err)
 
-			filesMap["/tmp/emoji.png"] = nil
+			filesMap["tmp/emoji.png"] = nil
 			err = processAttachments(c, &emojiLine, "", filesMap)
 			require.NoError(t, err)
 		})
diff --git a/server/channels/app/imports/import_validators.go b/server/channels/app/imports/import_validators.go
index e758da3148c..f414ae1e9c5 100644
--- a/server/channels/app/imports/import_validators.go
+++ b/server/channels/app/imports/import_validators.go
@@ -7,6 +7,7 @@ import (
 	"encoding/json"
 	"net/http"
 	"os"
+	"path/filepath"
 	"strings"
 	"unicode/utf8"
 
@@ -191,6 +192,10 @@ func ValidateChannelImportData(data *ChannelImportData) *model.AppError {
 
 func ValidateUserImportData(data *UserImportData) *model.AppError {
 	if data.ProfileImage != nil && data.ProfileImageData == nil {
+		// Check if the resolved path is within the expected base path.
+		if _, valid := ValidateAttachmentPathForImport(*data.ProfileImage, model.ExportDataDir); !valid {
+			return model.NewAppError("BulkImport", "app.import.validate_user_import_data.invalid_image_path.error", map[string]any{"Path": *data.ProfileImage}, "", http.StatusBadRequest)
+		}
 		if _, err := os.Stat(*data.ProfileImage); os.IsNotExist(err) {
 			return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusNotFound).Wrap(err)
 		} else if err != nil {
@@ -320,6 +325,11 @@ func ValidateUserImportData(data *UserImportData) *model.AppError {
 
 func ValidateBotImportData(data *BotImportData) *model.AppError {
 	if data.ProfileImage != nil && data.ProfileImageData == nil {
+		// Check if the resolved path is within the expected base path.
+		if _, valid := ValidateAttachmentPathForImport(*data.ProfileImage, model.ExportDataDir); !valid {
+			return model.NewAppError("BulkImport", "app.import.validate_user_import_data.invalid_image_path.error", map[string]any{"Path": *data.ProfileImage}, "", http.StatusBadRequest)
+		}
+
 		if _, err := os.Stat(*data.ProfileImage); os.IsNotExist(err) {
 			return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusNotFound).Wrap(err)
 		} else if err != nil {
@@ -487,6 +497,14 @@ func ValidateReplyImportData(data *ReplyImportData, parentCreateAt int64, maxPos
 		}
 	}
 
+	if data.Attachments != nil {
+		for _, attachment := range *data.Attachments {
+			if err := ValidateAttachmentImportData(&attachment); err != nil {
+				return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.attachment.error", nil, "", http.StatusNotFound).Wrap(err)
+			}
+		}
+	}
+
 	return nil
 }
 
@@ -537,6 +555,14 @@ func ValidatePostImportData(data *PostImportData, maxPostSize int) *model.AppErr
 		return model.NewAppError("BulkImport", "app.import.validate_post_import_data.props_too_large.error", nil, "", http.StatusBadRequest)
 	}
 
+	if data.Attachments != nil {
+		for _, attachment := range *data.Attachments {
+			if err := ValidateAttachmentImportData(&attachment); err != nil {
+				return model.NewAppError("BulkImport", "app.import.validate_post_import_data.attachment.error", nil, "", http.StatusNotFound).Wrap(err)
+			}
+		}
+	}
+
 	return nil
 }
 
@@ -653,6 +679,14 @@ func ValidateDirectPostImportData(data *DirectPostImportData, maxPostSize int) *
 		}
 	}
 
+	if data.Attachments != nil {
+		for _, attachment := range *data.Attachments {
+			if err := ValidateAttachmentImportData(&attachment); err != nil {
+				return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.attachment.error", nil, "", http.StatusNotFound).Wrap(err)
+			}
+		}
+	}
+
 	return nil
 }
 
@@ -671,6 +705,11 @@ func ValidateEmojiImportData(data *EmojiImportData) *model.AppError {
 		return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.image_missing.error", nil, "", http.StatusBadRequest)
 	}
 
+	// Check if the resolved path is within the expected base path.
+	if _, valid := ValidateAttachmentPathForImport(*data.Image, model.ExportDataDir); !valid {
+		return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.invalid_image_path.error", map[string]any{"Path": *data.Image}, "", http.StatusBadRequest)
+	}
+
 	if err := model.IsValidEmojiName(*data.Name); err != nil {
 		return err
 	}
@@ -749,3 +788,42 @@ func isValidGuestRoles(data UserImportData) bool {
 
 	return true
 }
+
+// ValidateAttachmentPathForImport joins 'path' to 'basePath' (defaulting to "." if empty) and ensures
+// the result does not escape the base directory. Returns the cleaned joined path (and true),
+// or an empty string (and false) if the result escapes the base.
+func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
+	if basePath == "" {
+		basePath = "."
+	}
+
+	joined := filepath.Join(basePath, path)
+
+	// Check if the resolved joined path is within basePath
+	rel, err := filepath.Rel(basePath, joined)
+	if err != nil {
+		return "", false
+	}
+	if strings.HasPrefix(rel, ".."+string(filepath.Separator)) || rel == ".." {
+		return "", false
+	}
+
+	return joined, true
+}
+
+func ValidateAttachmentImportData(data *AttachmentImportData) *model.AppError {
+	if data == nil {
+		return nil
+	}
+
+	if data.Path == nil || *data.Path == "" {
+		return nil
+	}
+
+	// Check if the resolved path is within the expected base path.
+	if _, valid := ValidateAttachmentPathForImport(*data.Path, model.ExportDataDir); !valid {
+		return model.NewAppError("BulkImport", "app.import.validate_attachment_import_data.invalid_path.error", map[string]any{"Path": *data.Path}, "", http.StatusBadRequest)
+	}
+
+	return nil
+}
diff --git a/server/channels/app/imports/import_validators_test.go b/server/channels/app/imports/import_validators_test.go
index 31567683edb..f975fe87088 100644
--- a/server/channels/app/imports/import_validators_test.go
+++ b/server/channels/app/imports/import_validators_test.go
@@ -499,6 +499,12 @@ func TestImportValidateUserImportData(t *testing.T) {
 	err = ValidateUserImportData(&data)
 	require.NotNil(t, err, "Validation should have failed due to not existing profile image file.")
 
+	// Invalid image path
+	data.ProfileImage = model.NewPointer("../invalid/path/file.jpg")
+	err = ValidateUserImportData(&data)
+	require.NotNil(t, err, "Validation should have failed due to invalid profile image file path.")
+	require.Equal(t, "app.import.validate_user_import_data.invalid_image_path.error", err.Id)
+
 	data.ProfileImage = nil
 
 	// Invalid Emails
@@ -623,8 +629,8 @@ func TestImportValidateUserImportData(t *testing.T) {
 	data.NotifyProps.MentionKeys = model.NewPointer("valid")
 	checkNoError(t, ValidateUserImportData(&data))
 
-	//Test the email batching interval validators
-	//Happy paths
+	// Test the email batching interval validators
+	// Happy paths
 	data.EmailInterval = model.NewPointer("immediately")
 	checkNoError(t, ValidateUserImportData(&data))
 
@@ -634,7 +640,7 @@ func TestImportValidateUserImportData(t *testing.T) {
 	data.EmailInterval = model.NewPointer("hour")
 	checkNoError(t, ValidateUserImportData(&data))
 
-	//Invalid values
+	// Invalid values
 	data.EmailInterval = model.NewPointer("invalid")
 	checkError(t, ValidateUserImportData(&data))
 
@@ -731,6 +737,12 @@ func TestImportValidateBotImportData(t *testing.T) {
 	data.Owner = model.NewPointer(strings.Repeat("abcdefghij", 7))
 	err = ValidateBotImportData(&data)
 	require.NotNil(t, err, "Should have failed due to too long OwnerID.")
+
+	// Invalid profile image path
+	data.ProfileImage = model.NewPointer("../invalid/path/file.jpg")
+	err = ValidateBotImportData(&data)
+	require.NotNil(t, err, "Should have failed due to invalid profile image file path.")
+	require.Equal(t, "app.import.validate_user_import_data.invalid_image_path.error", err.Id)
 }
 
 func TestImportValidateUserTeamsImportData(t *testing.T) {
@@ -943,6 +955,21 @@ func TestImportValidateReplyImportData(t *testing.T) {
 	}
 	err = ValidateReplyImportData(&data, parentCreateAt, maxPostSize)
 	require.NotNil(t, err, "Should have failed due to 0 create-at value.")
+
+	// Test with invalid attachment path.
+	data = ReplyImportData{
+		User:     model.NewPointer("username"),
+		Message:  model.NewPointer("message"),
+		CreateAt: model.NewPointer(model.GetMillis()),
+		Attachments: &[]AttachmentImportData{
+			{
+				Path: model.NewPointer("invalid/../../../path/to/file.txt"),
+			},
+		},
+	}
+	err = ValidateReplyImportData(&data, parentCreateAt, maxPostSize)
+	require.NotNil(t, err, "Should have failed due to invalid attachment path.")
+	require.Equal(t, err.Id, "app.import.validate_reply_import_data.attachment.error")
 }
 
 func TestImportValidatePostImportData(t *testing.T) {
@@ -1085,6 +1112,24 @@ func TestImportValidatePostImportData(t *testing.T) {
 		require.NotNil(t, err, "Should have failed due to long props.")
 		assert.Equal(t, err.Id, "app.import.validate_post_import_data.props_too_large.error")
 	})
+
+	t.Run("Test with invalid attachment path", func(t *testing.T) {
+		data := PostImportData{
+			Team:     model.NewPointer("teamname"),
+			Channel:  model.NewPointer("channelname"),
+			User:     model.NewPointer("username"),
+			Message:  model.NewPointer("message"),
+			CreateAt: model.NewPointer(model.GetMillis()),
+			Attachments: &[]AttachmentImportData{
+				{
+					Path: model.NewPointer("invalid/../../../path/to/file.txt"),
+				},
+			},
+		}
+		err := ValidatePostImportData(&data, maxPostSize)
+		require.NotNil(t, err)
+		assert.Equal(t, err.Id, "app.import.validate_post_import_data.attachment.error")
+	})
 }
 
 func TestImportValidateDirectChannelImportData(t *testing.T) {
@@ -1438,10 +1483,29 @@ func TestImportValidateDirectPostImportData(t *testing.T) {
 
 	err = ValidateDirectPostImportData(&data, maxPostSize)
 	require.Nil(t, err, "Validation should succeed with valid optional parameters")
+
+	// Test with invalid attachment path.
+	data = DirectPostImportData{
+		ChannelMembers: &[]string{
+			model.NewId(),
+			model.NewId(),
+		},
+		User:     model.NewPointer("username"),
+		Message:  model.NewPointer("message"),
+		CreateAt: model.NewPointer(model.GetMillis()),
+		Attachments: &[]AttachmentImportData{
+			{
+				Path: model.NewPointer("invalid/../../../path/to/file.txt"),
+			},
+		},
+	}
+	err = ValidateDirectPostImportData(&data, maxPostSize)
+	require.NotNil(t, err, "Should have failed due to invalid attachment path.")
+	require.Equal(t, err.Id, "app.import.validate_direct_post_import_data.attachment.error")
 }
 
 func TestImportValidateEmojiImportData(t *testing.T) {
-	var testCases = []struct {
+	testCases := []struct {
 		testName          string
 		name              *string
 		image             *string
@@ -1456,6 +1520,7 @@ func TestImportValidateEmojiImportData(t *testing.T) {
 		{"nil name", nil, model.NewPointer("/path/to/image"), true, false},
 		{"nil image", model.NewPointer("parrot2"), nil, true, false},
 		{"nil name and image", nil, nil, true, false},
+		{"invalid image path", model.NewPointer("parrot2"), model.NewPointer("../invalid/path/to/emoji.png"), true, false},
 	}
 
 	for _, tc := range testCases {
@@ -1485,7 +1550,7 @@ func checkNoError(t *testing.T, err *model.AppError) {
 }
 
 func TestIsValidGuestRoles(t *testing.T) {
-	var testCases = []struct {
+	testCases := []struct {
 		name     string
 		input    UserImportData
 		expected bool
@@ -1596,3 +1661,317 @@ func TestIsValidGuestRoles(t *testing.T) {
 		})
 	}
 }
+
+func TestValidateAttachmentPathForImport(t *testing.T) {
+	for _, tc := range []struct {
+		name         string
+		path         string
+		basePath     string
+		expectedPath string
+		expectedRes  bool
+	}{
+		{
+			name:         "valid relative path",
+			path:         "valid/path/to/attachment",
+			basePath:     "data",
+			expectedPath: "data/valid/path/to/attachment",
+			expectedRes:  true,
+		},
+		{
+			name:         "valid absolute path",
+			path:         "/valid/path/to/attachment",
+			basePath:     "data",
+			expectedPath: "data/valid/path/to/attachment",
+			expectedRes:  true,
+		},
+		{
+			name:         "valid relative path with empty base",
+			path:         "data/file.jpg",
+			basePath:     "",
+			expectedPath: "data/file.jpg",
+			expectedRes:  true,
+		},
+		{
+			name:         "absolute path with empty base is converted to relative",
+			path:         "/data/file.jpg",
+			basePath:     "",
+			expectedPath: "data/file.jpg",
+			expectedRes:  true,
+		},
+		{
+			name:         "valid path with dot segments",
+			path:         "path/./to/attachment",
+			basePath:     "data",
+			expectedPath: "data/path/to/attachment",
+			expectedRes:  true,
+		},
+		{
+			name:         "valid path with internal parent reference",
+			path:         "path/to/../to/attachment",
+			basePath:     "data",
+			expectedPath: "data/path/to/attachment",
+			expectedRes:  true,
+		},
+		{
+			name:         "valid path with filename containing dots",
+			path:         "path/to/file..txt",
+			basePath:     "data",
+			expectedPath: "data/path/to/file..txt",
+			expectedRes:  true,
+		},
+		{
+			name:         "valid path with default base path",
+			path:         "file.txt",
+			basePath:     "",
+			expectedPath: "file.txt",
+			expectedRes:  true,
+		},
+		{
+			name:         "valid path with spaces",
+			path:         "path/to/file with spaces.jpg",
+			basePath:     "data",
+			expectedPath: "data/path/to/file with spaces.jpg",
+			expectedRes:  true,
+		},
+		{
+			name:         "invalid path with parent directory traversal",
+			path:         "../file.txt",
+			basePath:     "data",
+			expectedPath: "",
+			expectedRes:  false,
+		},
+		{
+			name:         "invalid path with multiple parent directory traversal",
+			path:         "../../file.txt",
+			basePath:     "data",
+			expectedPath: "",
+			expectedRes:  false,
+		},
+		{
+			name:         "invalid path with parent directory traversal in middle",
+			path:         "path/../../file.txt",
+			basePath:     "data",
+			expectedPath: "",
+			expectedRes:  false,
+		},
+		{
+			name:         "invalid absolute path with traversal",
+			path:         "/path/../../../not/valid",
+			basePath:     "data",
+			expectedPath: "",
+			expectedRes:  false,
+		},
+		{
+			name:         "invalid relative path with substring in path",
+			path:         "../data_dir/attachment",
+			basePath:     "data",
+			expectedPath: "",
+			expectedRes:  false,
+		},
+		{
+			name:         "empty path",
+			path:         "",
+			basePath:     "data",
+			expectedPath: "data",
+			expectedRes:  true,
+		},
+		{
+			name:         "path is just a dot",
+			path:         ".",
+			basePath:     "data",
+			expectedPath: "data",
+			expectedRes:  true,
+		},
+		{
+			name:         "path with only parent reference",
+			path:         "..",
+			basePath:     "data",
+			expectedPath: "",
+			expectedRes:  false,
+		},
+		// Additional security test cases
+		{
+			name:         "valid path with double dots in filename",
+			path:         "....//file.txt",
+			basePath:     "data",
+			expectedPath: "data/..../file.txt",
+			expectedRes:  true, // Double dots in filename are valid, not traversal
+		},
+		{
+			name:         "valid path with quadruple dots in filename",
+			path:         "..../file.txt",
+			basePath:     "data",
+			expectedPath: "data/..../file.txt",
+			expectedRes:  true, // Quadruple dots in filename are valid, not traversal
+		},
+		{
+			name:         "valid path with backslashes (treated as literal on Unix)",
+			path:         "path\\..\\..\\file.txt",
+			basePath:     "data",
+			expectedPath: "data/path\\..\\..\\file.txt",
+			expectedRes:  true, // Backslashes are literal characters on Unix systems
+		},
+		{
+			name:         "valid path with mixed separators (backslash literal)",
+			path:         "path\\../file.txt",
+			basePath:     "data",
+			expectedPath: "data/path\\../file.txt",
+			expectedRes:  true, // Backslash is literal, only forward slash is normalized
+		},
+		{
+			name:         "valid URL encoded characters (treated as literals)",
+			path:         "%2e%2e%2ffile.txt",
+			basePath:     "data",
+			expectedPath: "data/%2e%2e%2ffile.txt",
+			expectedRes:  true, // URL encoding should be treated as literal characters
+		},
+		{
+			name:         "valid null byte in filename (treated as literal)",
+			path:         "file.txt\x00../etc/passwd",
+			basePath:     "data",
+			expectedPath: "data/file.txt\x00../etc/passwd",
+			expectedRes:  true, // Null bytes should be treated as literal characters
+		},
+		{
+			name:         "invalid complex traversal pattern",
+			path:         "./././../../../file.txt",
+			basePath:     "data",
+			expectedPath: "",
+			expectedRes:  false,
+		},
+		{
+			name:         "valid path with multiple slashes (normalized)",
+			path:         "path///../file.txt",
+			basePath:     "data",
+			expectedPath: "data/file.txt",
+			expectedRes:  true, // Multiple slashes get normalized, no traversal occurs
+		},
+		{
+			name:         "invalid deep traversal attempt",
+			path:         "../../../../../../../../../etc/passwd",
+			basePath:     "data",
+			expectedPath: "",
+			expectedRes:  false,
+		},
+		{
+			name:         "valid path with multiple internal dots",
+			path:         "path/to/file...with...dots.txt",
+			basePath:     "data",
+			expectedPath: "data/path/to/file...with...dots.txt",
+			expectedRes:  true,
+		},
+		{
+			name:         "invalid traversal with valid-looking suffix",
+			path:         "../trusted_NOT/secrets.txt",
+			basePath:     "/trusted",
+			expectedPath: "",
+			expectedRes:  false,
+		},
+		{
+			name:         "valid path with base path containing special chars",
+			path:         "file.txt",
+			basePath:     "data-dir_v1.0",
+			expectedPath: "data-dir_v1.0/file.txt",
+			expectedRes:  true,
+		},
+		{
+			name:         "valid Windows-style paths (backslashes literal on Unix)",
+			path:         "..\\..\\windows\\system32\\config",
+			basePath:     "data",
+			expectedPath: "data/..\\..\\windows\\system32\\config",
+			expectedRes:  true, // Backslashes are literal on Unix, no traversal
+		},
+		{
+			name:         "valid path with Unicode characters",
+			path:         "path/to/файл.txt",
+			basePath:     "data",
+			expectedPath: "data/path/to/файл.txt",
+			expectedRes:  true,
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			path, ok := ValidateAttachmentPathForImport(tc.path, tc.basePath)
+			require.Equal(t, tc.expectedPath, path)
+			require.Equal(t, tc.expectedRes, ok)
+		})
+	}
+}
+
+func TestValidateAttachmentImportData(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		data *AttachmentImportData
+		err  string
+	}{
+		{
+			name: "nil data",
+		},
+		{
+			name: "empty path",
+			data: &AttachmentImportData{},
+		},
+		{
+			name: "valid absolute path",
+			data: &AttachmentImportData{
+				Path: model.NewPointer("/valid/path/to/attachment"),
+			},
+		},
+		{
+			name: "invalid relative path",
+			data: &AttachmentImportData{
+				Path: model.NewPointer("../attachment"),
+			},
+			err: "BulkImport: app.import.validate_attachment_import_data.invalid_path.error",
+		},
+		{
+			name: "invalid relative path",
+			data: &AttachmentImportData{
+				Path: model.NewPointer("path/to/../../../attachment"),
+			},
+			err: "BulkImport: app.import.validate_attachment_import_data.invalid_path.error",
+		},
+
+		{
+			name: "invalid relative path",
+			data: &AttachmentImportData{
+				Path: model.NewPointer("../data_dir/attachment"),
+			},
+			err: "BulkImport: app.import.validate_attachment_import_data.invalid_path.error",
+		},
+
+		{
+			name: "valid relative path",
+			data: &AttachmentImportData{
+				Path: model.NewPointer("./path/to/attachment"),
+			},
+		},
+		{
+			name: "valid relative path",
+			data: &AttachmentImportData{
+				Path: model.NewPointer("path/../to/attachment"),
+			},
+		},
+		{
+			name: "valid relative path",
+			data: &AttachmentImportData{
+				Path: model.NewPointer("path/to/attachment"),
+			},
+		},
+		{
+			name: "valid relative path",
+			data: &AttachmentImportData{
+				Path: model.NewPointer("path/to/attachment/attachment..ext"),
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			err := ValidateAttachmentImportData(tc.data)
+			if tc.err != "" {
+				require.NotNil(t, err, "Expected error but got none")
+				require.EqualError(t, err, tc.err, "Expected error did not match")
+			} else {
+				require.Nil(t, err, "Expected no error but got one")
+			}
+		})
+	}
+}
diff --git a/server/cmd/mmctl/commands/import_test.go b/server/cmd/mmctl/commands/import_test.go
index 1c7907e81df..8ca40a3c7f3 100644
--- a/server/cmd/mmctl/commands/import_test.go
+++ b/server/cmd/mmctl/commands/import_test.go
@@ -475,4 +475,64 @@ func (s *MmctlUnitTestSuite) TestImportValidateCmdF() {
 		s.Require().Empty(res.Errors)
 		s.Equal("Validation complete\n", printer.GetLines()[2])
 	})
+
+	s.Run("invalid file attachment path", func() {
+		file, err := os.Create(importFilePath)
+		s.Require().NoError(err)
+
+		zipWr := zip.NewWriter(file)
+		wr, err := zipWr.Create("import.jsonl")
+		s.Require().NoError(err)
+
+		_, err = wr.Write([]byte(importBase))
+		s.Require().NoError(err)
+
+		_, err = wr.Write([]byte(`
+{"type":"post","post":{"team":"ad-1","channel":"iusto-9","user":"ashley.berry","message":"message","props":{},"create_at":1603398068740,"reactions":null,"replies":null,"attachments":[{"path": "data/../../invalid.jpg"}]}}`))
+		s.Require().NoError(err)
+
+		err = zipWr.Close()
+		s.Require().NoError(err)
+
+		err = file.Close()
+		s.Require().NoError(err)
+
+		printer.Clean()
+
+		s.client.
+			EXPECT().
+			GetUsers(context.TODO(), 0, 200, "").
+			Return(nil, &model.Response{}, nil).
+			Times(1)
+
+		s.client.
+			EXPECT().
+			GetAllTeams(context.TODO(), "", 0, 200).
+			Return(nil, &model.Response{}, nil).
+			Times(1)
+
+		s.client.
+			EXPECT().
+			GetOldClientConfig(context.TODO(), "").
+			Return(map[string]string{
+				"MaxPostSize": fmt.Sprintf("%d", model.PostMessageMaxRunesV2*2),
+			}, &model.Response{}, nil).
+			Times(1)
+
+		err = importValidateCmdF(s.client, ImportValidateCmd, []string{importFilePath})
+		s.Require().Nil(err)
+
+		s.Empty(printer.GetErrorLines())
+		s.Equal(Statistics{
+			Teams:          2,
+			Channels:       1,
+			DirectChannels: 1,
+			Users:          1,
+			Posts:          1,
+		}, printer.GetLines()[0].(Statistics))
+		res := printer.GetLines()[1].(ImportValidationResult)
+		s.Require().Len(res.Errors, 2)
+		s.Require().Equal("app.import.validate_post_import_data.attachment.error", res.Errors[0].Err.(*model.AppError).Id)
+		s.Equal("Validation complete\n", printer.GetLines()[2])
+	})
 }
diff --git a/server/i18n/en.json b/server/i18n/en.json
index 02105487ecb..282fcc1a188 100644
--- a/server/i18n/en.json
+++ b/server/i18n/en.json
@@ -5614,6 +5614,10 @@
     "id": "app.import.profile_image.read_data.app_error",
     "translation": "Failed to read profile image data."
   },
+  {
+    "id": "app.import.validate_attachment_import_data.invalid_path.error",
+    "translation": "Failed to validate attachment import data. Invalid path: \"{{.Path}}\""
+  },
   {
     "id": "app.import.validate_bot_import_data.owner_missing.error",
     "translation": "Bot owner is missing"
@@ -5678,6 +5682,10 @@
     "id": "app.import.validate_direct_channel_import_data.unknown_favoriter.error",
     "translation": "Direct channel can only be favorited by members. \"{{.Username}}\" is not a member."
   },
+  {
+    "id": "app.import.validate_direct_post_import_data.attachment.error",
+    "translation": "Failed to validate direct post attachment data."
+  },
   {
     "id": "app.import.validate_direct_post_import_data.channel_members_required.error",
     "translation": "Missing required direct post property: channel_members"
@@ -5722,10 +5730,18 @@
     "id": "app.import.validate_emoji_import_data.image_missing.error",
     "translation": "Import emoji image field missing or blank."
   },
+  {
+    "id": "app.import.validate_emoji_import_data.invalid_image_path.error",
+    "translation": "Import emoji image field has an invalid path: \"{{.Path}}\""
+  },
   {
     "id": "app.import.validate_emoji_import_data.name_missing.error",
     "translation": "Import emoji name field missing or blank."
   },
+  {
+    "id": "app.import.validate_post_import_data.attachment.error",
+    "translation": "Failed to validate post attachment data."
+  },
   {
     "id": "app.import.validate_post_import_data.channel_missing.error",
     "translation": "Missing required Post property: Channel."
@@ -5782,6 +5798,10 @@
     "id": "app.import.validate_reaction_import_data.user_missing.error",
     "translation": "Missing required Reaction property: User."
   },
+  {
+    "id": "app.import.validate_reply_import_data.attachment.error",
+    "translation": "Failed to validate reply attachment data."
+  },
   {
     "id": "app.import.validate_reply_import_data.create_at_missing.error",
     "translation": "Missing required Reply property: create_at."
@@ -5946,6 +5966,10 @@
     "id": "app.import.validate_user_import_data.guest_roles_conflict.error",
     "translation": "User roles are not consistent with guest status."
   },
+  {
+    "id": "app.import.validate_user_import_data.invalid_image_path.error",
+    "translation": "User profile image path is invalid: \"{{.Path}}\""
+  },
   {
     "id": "app.import.validate_user_import_data.last_name_length.error",
     "translation": "User Last Name is too long."


Reviews:
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T15:41:29Z
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T15:44:17Z
user: login: lieut-data
body: Thanks, @streamer45! A few questions below.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-27T16:50:48Z
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T17:19:49Z
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T17:20:33Z
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T17:20:37Z
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T17:20:42Z
user: login: cpoile
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-27T17:27:13Z
user: login: cpoile
body: One non-blocking question
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-27T17:40:50Z
user: login: enzowritescode
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T18:27:55Z
user: login: lieut-data
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-27T18:49:10Z
user: login: enzowritescode
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T18:58:29Z
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T06:15:47Z
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T06:31:00Z
user: login: jupenur
body: Wouldn't `line.User.ProfileImage`, `line.Bot.ProfileImage`, and `line.Emoji.Image` in `processAttachments` also need additional validation?
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-28T08:12:41Z
user: login: jupenur
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-28T11:03:56Z
user: login: lieut-data
body: Three comments, but only strictly blocking around the discussion in `ValidateAttachmentPathForImport`.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-28T13:19:14Z
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T13:24:22Z
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T13:26:32Z
user: login: streamer45
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T14:32:18Z
user: login: lieut-data
body: Unblocking after discussion with @streamer45. Will want to circle back on some of this later :)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-28T16:24:50Z
user: login: enzowritescode
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T18:34:44Z
user: login: DHaussermann
body: Tested and passed 
- Repeated testing above https://github.com/mattermost/mattermost/pull/31201#issuecomment-2913703190
- Test both with previous import data and data eported while this branch was deployed
- Post data looks normal
- All attachments are available and functional
- Thumbnails and in-line display work normally
- Files are valid when downloaded (including zip archive)
- Profile images export / import fine
- Tested all channel types and root posts and well as reply posts

LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T20:01:27Z

Review comments:
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
path: server/channels/app/import.go
user: login: streamer45
body: Using this since errors from this function are not fatal in the context of an import job (they just get logged as warnings). Still we'd like to process all the files/paths and not stop at the first failure.
created_at: 2025-05-27T15:41:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
+					multiErr = multierror.Append(multiErr, fmt.Errorf("attachment %q not found in map", originalPath))
+					continue
path: server/channels/app/import.go
user: login: streamer45
body: Continuing so that we process and blank all the invalid file paths. This is technically redundant since these will still go through validation later in the process (e.g. in https://github.com/mattermost/mattermost/blob/93185376965a1952d54ddcb1d305310247ba8b3f/server/channels/app/import_functions.go#L1788), but I wanted to be on the safe side. Happy to review of course.
created_at: 2025-05-27T15:44:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
path: server/channels/app/import.go
user: login: lieut-data
body: /slightly/ weird to see the assign before checking `valid`, but I understand that this clears the path.
created_at: 2025-05-27T16:29:58Z
author_association: MEMBER
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
path: server/channels/app/import.go
user: login: lieut-data
body: Ooh, I admit I have never used `%q` before -- tracking this for the future.
created_at: 2025-05-27T16:42:20Z
author_association: MEMBER
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
path: server/channels/app/import.go
user: login: lieut-data
body: `*f.Path` has potentially changed here, but `filesMap` has not -- will we always trip this block if `ValidateAttachmentPathForImport` cleans some part of the path but doesn't outright reject it?
created_at: 2025-05-27T16:42:50Z
author_association: MEMBER
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
+					multiErr = multierror.Append(multiErr, fmt.Errorf("attachment %q not found in map", originalPath))
+					continue
path: server/channels/app/import.go
user: login: lieut-data
body: Is it clearer if we make `processAttachmentPaths` fail fast, and have the caller bail as soon as possible? Or is there a valid use case to continuing on while ignoring the bad data?
created_at: 2025-05-27T16:46:20Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,7 @@ import (
 	"strings"
 	"sync"
 
+	"github.com/hashicorp/go-multierror"
path: server/channels/app/import.go
user: login: lieut-data
body: 0/5, but why not https://pkg.go.dev/errors#Join?
created_at: 2025-05-27T16:47:41Z
author_association: MEMBER
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
+					multiErr = multierror.Append(multiErr, fmt.Errorf("attachment %q not found in map", originalPath))
+					continue
 				}
 			}
 		}
 	}
 
-	return nil
+	if multiErr == nil {
+		return nil
+	}
+
+	return multiErr
path: server/channels/app/import.go
user: login: lieut-data
body: Worth using https://pkg.go.dev/github.com/hashicorp/go-multierror#Error.ErrorOrNil instead?
created_at: 2025-05-27T16:48:05Z
author_association: MEMBER
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
+					multiErr = multierror.Append(multiErr, fmt.Errorf("attachment %q not found in map", originalPath))
+					continue
path: server/channels/app/import.go
user: login: streamer45
body: I believe the use case was not to fail the entire import process (which can take hours) for a couple of failing post attachments. Failing fast here would be much simpler, I agree.
created_at: 2025-05-27T17:19:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
path: server/channels/app/import.go
user: login: streamer45
body: That shouldn't be substantially different from before. `filesMap` expects the joined path, so we are passing our best attempt at that.
created_at: 2025-05-27T17:20:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
path: server/channels/app/import.go
user: login: streamer45
body: Yeah. Ideally I would avoid validation in this place so we could also remove the "hack" of blanking the path, but since we effectively join the paths here, it felt slightly more robust.
created_at: 2025-05-27T17:20:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +14,7 @@ import (
 	"strings"
 	"sync"
 
+	"github.com/hashicorp/go-multierror"
path: server/channels/app/import.go
user: login: streamer45
body: No reason other than I know we use `multierror` quite a lot throughout our code. I'll simplify, though.
created_at: 2025-05-27T17:20:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
+					multiErr = multierror.Append(multiErr, fmt.Errorf("attachment %q not found in map", originalPath))
+					continue
path: server/channels/app/import.go
user: login: cpoile
body: I'd vote to not fail fast in bulk ops like this.
created_at: 2025-05-27T17:27:13Z
author_association: MEMBER
diff_hunk: @@ -749,3 +774,35 @@ func isValidGuestRoles(data UserImportData) bool {
 
 	return true
 }
+
+// ValidateAttachmentPathForImport checks if the provided path is valid for import.
+// It ensures that the path is cleaned and resolved against a base path.
+// It returns the resolved path and a boolean indicating if the path is valid.
+func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
+	cleanedPath := filepath.Clean(path)
+	resolvedPath := filepath.Join(basePath, cleanedPath)
path: server/channels/app/imports/import_validators.go
user: login: cpoile
body: Are we treating imported filepaths as "input"? If so, we should EvalSymlinks just in case (who knows if the system has a link somewhere it shouldn't).  (correct me if I'm wrong ofc)
created_at: 2025-05-27T17:40:32Z
author_association: MEMBER
diff_hunk: @@ -749,3 +774,35 @@ func isValidGuestRoles(data UserImportData) bool {
 
 	return true
 }
+
+// ValidateAttachmentPathForImport checks if the provided path is valid for import.
+// It ensures that the path is cleaned and resolved against a base path.
+// It returns the resolved path and a boolean indicating if the path is valid.
+func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
+	cleanedPath := filepath.Clean(path)
+	resolvedPath := filepath.Join(basePath, cleanedPath)
path: server/channels/app/imports/import_validators.go
user: login: enzowritescode
body: +1 for checking symlinks
created_at: 2025-05-27T18:27:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
path: server/channels/app/import.go
user: login: lieut-data
body: Yep, that's fair that this doesn't change relative to the base code -- @cpoile, maybe you can explain my misunderstanding here? It seems like this should always fail unless `filesMap` has been populated with the joined path already?
created_at: 2025-05-27T18:49:09Z
author_association: MEMBER
diff_hunk: @@ -749,3 +774,35 @@ func isValidGuestRoles(data UserImportData) bool {
 
 	return true
 }
+
+// ValidateAttachmentPathForImport checks if the provided path is valid for import.
+// It ensures that the path is cleaned and resolved against a base path.
+// It returns the resolved path and a boolean indicating if the path is valid.
+func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
+	cleanedPath := filepath.Clean(path)
+	resolvedPath := filepath.Join(basePath, cleanedPath)
+
+	// Check if the resolved path is within the expected base path.
path: server/channels/app/imports/import_validators.go
user: login: enzowritescode
body: (From an internal discussion)

We need to ensure the prefix ends with a `/` so that `/trusted_NOT` is not a trusted path against `/trusted`
created_at: 2025-05-27T18:58:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -749,3 +774,35 @@ func isValidGuestRoles(data UserImportData) bool {
 
 	return true
 }
+
+// ValidateAttachmentPathForImport checks if the provided path is valid for import.
+// It ensures that the path is cleaned and resolved against a base path.
+// It returns the resolved path and a boolean indicating if the path is valid.
+func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
+	cleanedPath := filepath.Clean(path)
+	resolvedPath := filepath.Join(basePath, cleanedPath)
path: server/channels/app/imports/import_validators.go
user: login: streamer45
body: From the [internal discussion](https://hub.mattermost.com/private-core/pl/8fs43ghripft5fxrnymcq6oure), we decided to revisit this later since having symlinks pointing outside a safe tree should be discouraged and it's not strictly an application issue.
created_at: 2025-05-28T06:15:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
path: server/channels/app/import.go
user: login: streamer45
body: @lieut-data If it helps, imports should typically go through `mmctl import validate`, which will raise an error if there are any invalid paths. So ideally, we shouldn't encounter these issues during a real run. Given that our app generates the export files, I think focusing on the happy path should be sufficient.

>  It seems like this should always fail unless filesMap has been populated with the joined path already?

`filesMap` is used to map the location of data within the zip reader to the corresponding file path specified in the import line. If the mapping isn't found (i.e., it fails), we fall back to opening that path from the local file system.

https://github.com/mattermost/mattermost/blob/93185376965a1952d54ddcb1d305310247ba8b3f/server/channels/app/import_functions.go#L1605-L1606
created_at: 2025-05-28T06:30:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -749,3 +774,43 @@ func isValidGuestRoles(data UserImportData) bool {
 
 	return true
 }
+
+// ValidateAttachmentPathForImport joins 'path' to 'basePath' (defaulting to "." if empty) and ensures
+// the result does not escape the base directory. Returns the cleaned joined path (and true),
+// or an empty string (and false) if the result escapes the base.
+func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
+	if basePath == "" {
+		basePath = "."
+	}
+
+	joined := filepath.Join(basePath, path)
+	cleanJoined := filepath.Clean(joined)
path: server/channels/app/imports/import_validators.go
user: login: jupenur
body: `filepath.Join` internally calls `Clean` on the result; no need to call `Clean` separately.
created_at: 2025-05-28T07:00:49Z
author_association: MEMBER
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
path: server/channels/app/import.go
user: login: lieut-data
body: Yep, that makes sense: in the happy path, the `filesMap` is already effectively populated using a key that joins the base path and `f.Path`, so once we join again, we should "find the same key". 

Maybe there's a way to avoid relying on `f.Name` in the first place? As in, if we trust the `map` completely, we can remove that fallback to the local file system and sidestep much of the impetus behind this PR?
created_at: 2025-05-28T13:05:54Z
author_association: MEMBER
diff_hunk: @@ -775,19 +789,26 @@ func isValidGuestRoles(data UserImportData) bool {
 	return true
 }
 
-// ValidateAttachmentPathForImport checks if the provided path is valid for import.
-// It ensures that the path is cleaned and resolved against a base path.
-// It returns the resolved path and a boolean indicating if the path is valid.
+// ValidateAttachmentPathForImport joins 'path' to 'basePath' (defaulting to "." if empty) and ensures
+// the result does not escape the base directory. Returns the cleaned joined path (and true),
+// or an empty string (and false) if the result escapes the base.
 func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
-	cleanedPath := filepath.Clean(path)
-	resolvedPath := filepath.Join(basePath, cleanedPath)
+	if basePath == "" {
+		basePath = "."
+	}
path: server/channels/app/imports/import_validators.go
user: login: lieut-data
body: If `basePath` is empty, should we just fail?
created_at: 2025-05-28T13:08:38Z
author_association: MEMBER
diff_hunk: @@ -775,19 +789,26 @@ func isValidGuestRoles(data UserImportData) bool {
 	return true
 }
 
-// ValidateAttachmentPathForImport checks if the provided path is valid for import.
-// It ensures that the path is cleaned and resolved against a base path.
-// It returns the resolved path and a boolean indicating if the path is valid.
+// ValidateAttachmentPathForImport joins 'path' to 'basePath' (defaulting to "." if empty) and ensures
+// the result does not escape the base directory. Returns the cleaned joined path (and true),
+// or an empty string (and false) if the result escapes the base.
 func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
-	cleanedPath := filepath.Clean(path)
-	resolvedPath := filepath.Join(basePath, cleanedPath)
+	if basePath == "" {
+		basePath = "."
+	}
 
-	// Check if the resolved path is within the expected base path.
-	if !strings.HasPrefix(resolvedPath, basePath) {
+	joined := filepath.Join(basePath, path)
+
+	// Check if the resolved joined path is within basePath
+	rel, err := filepath.Rel(basePath, joined)
+	if err != nil {
+		return "", false
+	}
+	if strings.HasPrefix(rel, ".."+string(filepath.Separator)) || rel == ".." {
 		return "", false
 	}
path: server/channels/app/imports/import_validators.go
user: login: lieut-data
body: This seems correct, but oddly counter intuitive. The comment would have to be something like, "Check if the relative path between `basePath` and `joined` requires accessing the parent directory, thus implying the joined path is not within `basePath`." I /think/ that works, but I admit I'm not confident.

Acknowledging the discussed limitations of `strings.HasPrefix`, would it make sense to just check 
```go
// Ensure basePath ends with separator for safer prefix checking
if !strings.HasSuffix(basePath, string(filepath.Separator)) {
    basePath = basePath + string(filepath.Separator)
}

// Old check...
if !strings.HasPrefix(resolvedPath, basePath)
```

created_at: 2025-05-28T13:12:58Z
author_association: MEMBER
diff_hunk: @@ -775,19 +789,26 @@ func isValidGuestRoles(data UserImportData) bool {
 	return true
 }
 
-// ValidateAttachmentPathForImport checks if the provided path is valid for import.
-// It ensures that the path is cleaned and resolved against a base path.
-// It returns the resolved path and a boolean indicating if the path is valid.
+// ValidateAttachmentPathForImport joins 'path' to 'basePath' (defaulting to "." if empty) and ensures
+// the result does not escape the base directory. Returns the cleaned joined path (and true),
+// or an empty string (and false) if the result escapes the base.
 func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
-	cleanedPath := filepath.Clean(path)
-	resolvedPath := filepath.Join(basePath, cleanedPath)
+	if basePath == "" {
+		basePath = "."
+	}
path: server/channels/app/imports/import_validators.go
user: login: streamer45
body: I think we'd have to fully deprecate `app.BulkImport()` and fix all its occurrences since it defaults to an empty base import path. 
created_at: 2025-05-28T13:24:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,20 +46,36 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
 	if files == nil {
 		return nil
 	}
+
 	var ok bool
+	var multiErr *multierror.Error
 	for i, f := range *files {
 		if f.Path != nil {
-			path := filepath.Join(basePath, *f.Path)
+			originalPath := *f.Path
+
+			path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
+
 			*f.Path = path
+
+			if !valid {
+				multiErr = multierror.Append(multiErr, fmt.Errorf("invalid attachment path %q", originalPath))
+				continue
+			}
+
 			if len(filesMap) > 0 {
-				if (*files)[i].Data, ok = filesMap[path]; !ok {
-					return fmt.Errorf("attachment %q not found in map", path)
+				if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
path: server/channels/app/import.go
user: login: streamer45
body: I don't feel super comfortable making that decision in a time of urgency. I fear this was designed this way for a good reason and it would require some time for us to go back to it and carefully review.
created_at: 2025-05-28T13:26:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -775,19 +789,26 @@ func isValidGuestRoles(data UserImportData) bool {
 	return true
 }
 
-// ValidateAttachmentPathForImport checks if the provided path is valid for import.
-// It ensures that the path is cleaned and resolved against a base path.
-// It returns the resolved path and a boolean indicating if the path is valid.
+// ValidateAttachmentPathForImport joins 'path' to 'basePath' (defaulting to "." if empty) and ensures
+// the result does not escape the base directory. Returns the cleaned joined path (and true),
+// or an empty string (and false) if the result escapes the base.
 func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
-	cleanedPath := filepath.Clean(path)
-	resolvedPath := filepath.Join(basePath, cleanedPath)
+	if basePath == "" {
+		basePath = "."
+	}
 
-	// Check if the resolved path is within the expected base path.
-	if !strings.HasPrefix(resolvedPath, basePath) {
+	joined := filepath.Join(basePath, path)
+
+	// Check if the resolved joined path is within basePath
+	rel, err := filepath.Rel(basePath, joined)
+	if err != nil {
+		return "", false
+	}
+	if strings.HasPrefix(rel, ".."+string(filepath.Separator)) || rel == ".." {
 		return "", false
 	}
path: server/channels/app/imports/import_validators.go
user: login: streamer45
body: @lieut-data To be honest, none of this path handling feels intuitive to me :p

That logic seems exactly the same as @enzowritescode posted yesterday, which unfortunately [didn't work](https://hub.mattermost.com/private-core/pl/p6rouidyxbfr8dpgwfmpcitemy) as it was causing false positives in the case of an empty path (e.g. `data/file.jpg` would be invalid).

I provided extensive test cases to ensure we are not breaking anything and at the same time failing for clearly bogus paths. Unfortunately, we have to work with the constraints of the higher-level implementation which make this a little trickier than we'd like.

created_at: 2025-05-28T14:32:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: streamer45
created_at: 2025-05-27T16:19:30Z
body: /cc @cpoile 
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-27T16:24:22Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres](https://github.com/mattermost/mattermost/actions/runs/15279627317)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-27T16:24:23Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres with binary parameters](https://github.com/mattermost/mattermost/actions/runs/15279627317)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres with binary parameters (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-27T16:24:23Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:MySQL](https://github.com/mattermost/mattermost/actions/runs/15279627317)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>MySQL (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: unified-ci-app[bot]
created_at: 2025-05-27T16:26:17Z
body: E2E test triggered successfully for PR #31201. The corresponding commit's status check will be available shortly.

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-27T16:27:10Z
body: E2E test run is starting for commit `93185376965a1952d54ddcb1d305310247ba8b3f`.
To check the run progress:
- Cypress: look for commit status `E2E Tests/test` or the access the [Automation Dashboard Cycle URL](https://automation-dashboard.vercel.app/cycle/15280607002_1-9318537-pr-onprem-ent).
- Playwright: look for commit status `E2E Tests/test-playwright`.

You can also look at the [E2E test's Workflow Run URL](https://github.com/mattermost/mattermost/actions/runs/15280607002) (run ID `15280607002`).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-27T17:13:54Z
body: E2E test has completed for commit `93185376965a1952d54ddcb1d305310247ba8b3f`.
Results summary:
- Cypress: pass rate is `100.00%` (see [Automation Dashboard](https://automation-dashboard.vercel.app/cycle/15280607002_1-9318537-pr-onprem-ent) and commit status check `E2E Tests/test`).
- Playwright: pass rate is `100.00%` (see [Playwright Report URL](https://mattermost-cypress-report.s3.amazonaws.com/server-pr-31201/e2e-reports/playwright/15280607002/results/reporter/index.html) and commit status check `E2E Tests/test-playwright`).

The run summary artifacts are available in the corresponding [Workflow Run](https://github.com/mattermost/mattermost/actions/runs/15280607002).

author_association: NONE
user: login: DHaussermann
created_at: 2025-05-27T19:15:58Z
body: Tested the import with focus on post attachments in different channel types and in root post and reply threads
- Dataset
  - Create posts in various channel type (pub, priv, DMs and GMs)
  - Include root posts and reply posts
  - All posts have attachments (various image type, video files and zip)

- Existing data import
  -  Create data export on prev release
  - Import data on PR branch
  - Verify all posts and attachment are valid and working (including extracting the zip) 
  
- New data export import
  - Create data on PR branch
  - Import data on PR branch  
   - Verify all posts and attachment are valid and working (including extracting the zip) 
   - As a precaution also included profile images (even though they are user data and not attachments) 
   - Explored data post and attachments across multiple teams for anything that looked out of order  
   
  LGTM!
  
  @streamer45 please ping me if there are any follow-up commits and I'm happy to revisit
  Also let me know if there are any additional suggestion for functional testing
author_association: CONTRIBUTOR
user: login: unified-ci-app[bot]
created_at: 2025-05-28T07:06:18Z
body: E2E tests requested: they will start automatically when status check `Enterprise CI/docker-image` is successful.

author_association: CONTRIBUTOR
user: login: unified-ci-app[bot]
created_at: 2025-05-28T07:08:17Z
body: Docker image is now available: E2E test triggered successfully for PR #31201. The corresponding commit's status check will be available shortly.

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-28T07:09:08Z
body: E2E test run is starting for commit `9ac11d017f6828f1a799a2bd89dc5af393f72eaf`.
To check the run progress:
- Cypress: look for commit status `E2E Tests/test` or the access the [Automation Dashboard Cycle URL](https://automation-dashboard.vercel.app/cycle/15293783268_1-9ac11d0-pr-onprem-ent).
- Playwright: look for commit status `E2E Tests/test-playwright`.

You can also look at the [E2E test's Workflow Run URL](https://github.com/mattermost/mattermost/actions/runs/15293783268) (run ID `15293783268`).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T07:33:07Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:MySQL](https://github.com/mattermost/mattermost/actions/runs/15293415520)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>MySQL (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T07:33:07Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres with binary parameters](https://github.com/mattermost/mattermost/actions/runs/15293415520)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres with binary parameters (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T07:33:09Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres](https://github.com/mattermost/mattermost/actions/runs/15293415520)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T07:56:26Z
body: E2E test has completed for commit `9ac11d017f6828f1a799a2bd89dc5af393f72eaf`.
Results summary:
- Cypress: pass rate is `99.93%` (see [Automation Dashboard](https://automation-dashboard.vercel.app/cycle/15293783268_1-9ac11d0-pr-onprem-ent) and commit status check `E2E Tests/test`).
- Playwright: pass rate is `100.00%` (see [Playwright Report URL](https://mattermost-cypress-report.s3.amazonaws.com/server-pr-31201/e2e-reports/playwright/15293783268/results/reporter/index.html) and commit status check `E2E Tests/test-playwright`).

The run summary artifacts are available in the corresponding [Workflow Run](https://github.com/mattermost/mattermost/actions/runs/15293783268).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T08:26:41Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres with binary parameters](https://github.com/mattermost/mattermost/actions/runs/15294425211)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres with binary parameters (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T08:26:41Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres](https://github.com/mattermost/mattermost/actions/runs/15294425211)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T08:26:44Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:MySQL](https://github.com/mattermost/mattermost/actions/runs/15294425211)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>MySQL (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T11:42:19Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres with binary parameters](https://github.com/mattermost/mattermost/actions/runs/15298373926)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres with binary parameters (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T11:42:19Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:MySQL](https://github.com/mattermost/mattermost/actions/runs/15298373926)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>MySQL (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T11:42:20Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres](https://github.com/mattermost/mattermost/actions/runs/15298373926)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T12:43:20Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:MySQL](https://github.com/mattermost/mattermost/actions/runs/15299525245)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>MySQL (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T12:43:23Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres](https://github.com/mattermost/mattermost/actions/runs/15299525245)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T12:43:24Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres with binary parameters](https://github.com/mattermost/mattermost/actions/runs/15299525245)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres with binary parameters (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: unified-ci-app[bot]
created_at: 2025-05-28T12:45:24Z
body: E2E test triggered successfully for PR #31201. The corresponding commit's status check will be available shortly.

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-28T12:46:24Z
body: E2E test run is starting for commit `d3c4e63601937a3ea0da4ccf46854ad433fdb4c6`.
To check the run progress:
- Cypress: look for commit status `E2E Tests/test` or the access the [Automation Dashboard Cycle URL](https://automation-dashboard.vercel.app/cycle/15300480937_1-d3c4e63-pr-onprem-ent).
- Playwright: look for commit status `E2E Tests/test-playwright`.

You can also look at the [E2E test's Workflow Run URL](https://github.com/mattermost/mattermost/actions/runs/15300480937) (run ID `15300480937`).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T13:33:17Z
body: E2E test has completed for commit `d3c4e63601937a3ea0da4ccf46854ad433fdb4c6`.
Results summary:
- Cypress: pass rate is `100.00%` (see [Automation Dashboard](https://automation-dashboard.vercel.app/cycle/15300480937_1-d3c4e63-pr-onprem-ent) and commit status check `E2E Tests/test`).
- Playwright: pass rate is `100.00%` (see [Playwright Report URL](https://mattermost-cypress-report.s3.amazonaws.com/server-pr-31201/e2e-reports/playwright/15300480937/results/reporter/index.html) and commit status check `E2E Tests/test-playwright`).

The run summary artifacts are available in the corresponding [Workflow Run](https://github.com/mattermost/mattermost/actions/runs/15300480937).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T17:53:32Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres](https://github.com/mattermost/mattermost/actions/runs/15306252585)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T17:53:34Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:Postgres with binary parameters](https://github.com/mattermost/mattermost/actions/runs/15306252585)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>Postgres with binary parameters (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/PostgreSQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T17:53:44Z
body: #### ⚠️  One or more flaky tests detected ⚠️
* Failing job: [github.com/mattermost/mattermost:MySQL](https://github.com/mattermost/mattermost/actions/runs/15306252585)
* Double check your code to ensure you haven't introduced a flaky test.
* If this seems to be unrelated to your changes, submit a separate pull request to skip the flaky tests (e.g. [23360](https://github.com/mattermost/mattermost/pull/23360)) and file JIRA ticket (e.g. [MM-52743](https://mattermost.atlassian.net/browse/MM-52743)) for later investigation.

<table><tr><th>Test</th><th>Retries</th></tr><tr><td colspan="2"><strong>MySQL (Results)</strong></td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore/LocalCache+MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/localcachelayer.TestPostStore</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL/GetNthRecentPostTime</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore/MySQL</td><td>1</td></tr><tr><td>com/mattermost/mattermost/server/v8/channels/store/sqlstore.TestPostStore</td><td>1</td></tr></table>

[MM-52743]: https://mattermost.atlassian.net/browse/MM-52743?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: NONE
user: login: streamer45
created_at: 2025-05-28T18:34:39Z
body: @DHaussermann Could you give it another pass? Code changes should be mostly finalized at this point.
author_association: CONTRIBUTOR
user: login: unified-ci-app[bot]
created_at: 2025-05-28T23:04:26Z
body: E2E test triggered successfully for PR #31201. The corresponding commit's status check will be available shortly.

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-28T23:05:17Z
body: E2E test run is starting for commit `cef0e8c5d45d47b574fa17547823573dbe8a076e`.
To check the run progress:
- Cypress: look for commit status `E2E Tests/test` or the access the [Automation Dashboard Cycle URL](https://automation-dashboard.vercel.app/cycle/15312345452_1-cef0e8c-pr-onprem-ent).
- Playwright: look for commit status `E2E Tests/test-playwright`.

You can also look at the [E2E test's Workflow Run URL](https://github.com/mattermost/mattermost/actions/runs/15312345452) (run ID `15312345452`).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T23:52:07Z
body: E2E test has completed for commit `cef0e8c5d45d47b574fa17547823573dbe8a076e`.
Results summary:
- Cypress: pass rate is `99.93%` (see [Automation Dashboard](https://automation-dashboard.vercel.app/cycle/15312345452_1-cef0e8c-pr-onprem-ent) and commit status check `E2E Tests/test`).
- Playwright: pass rate is `100.00%` (see [Playwright Report URL](https://mattermost-cypress-report.s3.amazonaws.com/server-pr-31201/e2e-reports/playwright/15312345452/results/reporter/index.html) and commit status check `E2E Tests/test-playwright`).

The run summary artifacts are available in the corresponding [Workflow Run](https://github.com/mattermost/mattermost/actions/runs/15312345452).

author_association: NONE
user: login: streamer45
created_at: 2025-05-29T05:44:02Z
body: Single failing e2e test looks flaky as it's unrelated for these changes. 

![image](https://github.com/user-attachments/assets/01f0a76e-20e8-48e7-8a26-5d2e097f0f2b)


Overriding.
author_association: CONTRIBUTOR
user: login: mattermost-build
created_at: 2025-05-29T05:44:52Z
body: Cherry pick is scheduled.
author_association: CONTRIBUTOR
user: login: streamer45
created_at: 2025-05-29T07:49:18Z
body: /cherry-pick release-10.8
author_association: CONTRIBUTOR
user: login: mattermost-build
created_at: 2025-05-29T07:49:20Z
body: Cherry pick is scheduled.
author_association: CONTRIBUTOR
user: login: mattermost-build
created_at: 2025-05-29T07:49:30Z
body: Error trying doing the automated Cherry picking. Please do this manually

```
+++ Updating remotes...
Fetching upstream
hostfile_replace_entries: mkstemp: Read-only file system
update_known_hosts: hostfile_replace_entries failed for /app/.ssh/known_hosts: Read-only file system
From github.com:mattermost/mattermost
   81d3273bc7..79bf1c34db  master       -> upstream/master
   6597b57a72..e0e0cb356d  release-10.9 -> upstream/release-10.9
Fetching upstream
hostfile_replace_entries: mkstemp: Read-only file system
update_known_hosts: hostfile_replace_entries failed for /app/.ssh/known_hosts: Read-only file system
+++ Updating remotes done...
+++ Creating local branch automated-cherry-pick-of-mattermost-#31201-upstream-release-10.8-1748504962
Switched to a new branch 'automated-cherry-pick-of-mattermost-#31201-upstream-release-10.8-1748504962'
Branch 'automated-cherry-pick-of-mattermost-#31201-upstream-release-10.8-1748504962' set up to track remote branch 'release-10.8' from 'upstream'.

+++ About to attempt cherry pick of PR #31201 with merge commit d38c27f96fcf754c36f231d1f2e9dbd48ad40bab.

Auto-merging server/channels/app/import_functions.go
Auto-merging server/i18n/en.json
CONFLICT (content): Merge conflict in server/i18n/en.json
error: could not apply d38c27f96f... [MM-64402] Improve validation of imported attachments (#31201)
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".

+++ Conflicts detected:

UU server/i18n/en.json
Aborting.

+++ Aborting in-progress git cherry-pick.

+++ Returning you to the master branch and cleaning up.

```


[MM-64402]: https://mattermost.atlassian.net/browse/MM-64402?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: CONTRIBUTOR
user: login: amyblais
created_at: 2025-06-02T09:41:21Z
body: /cherry-pick release-10.7
author_association: MEMBER
user: login: mattermost-build
created_at: 2025-06-02T09:41:24Z
body: Cherry pick is scheduled.
author_association: CONTRIBUTOR
user: login: mattermost-build
created_at: 2025-06-02T09:41:34Z
body: Error trying doing the automated Cherry picking. Please do this manually

```
+++ Updating remotes...
Fetching upstream
hostfile_replace_entries: mkstemp: Read-only file system
update_known_hosts: hostfile_replace_entries failed for /app/.ssh/known_hosts: Read-only file system
From github.com:mattermost/mattermost
   990535f79c..378df121ad  MM-64299-disable-guest-invite-in-abac-channels -> upstream/MM-64299-disable-guest-invite-in-abac-channels
   43ab00c569..7b56822f5d  MM-64330-filter-abac-users-in-channel-invite -> upstream/MM-64330-filter-abac-users-in-channel-invite
 * [new branch]            MM-64462     -> upstream/MM-64462
   d1af60ff04..1ffe1da7b4  cherry-pick-6f26ad5-into-release-10.9 -> upstream/cherry-pick-6f26ad5-into-release-10.9
   a457d3b73c..d2292a13d2  master       -> upstream/master
   2113064299..6815ec9955  release-10.9 -> upstream/release-10.9
   d92fdb1cc6..bdb33fc665  user-export  -> upstream/user-export
Fetching upstream
hostfile_replace_entries: mkstemp: Read-only file system
update_known_hosts: hostfile_replace_entries failed for /app/.ssh/known_hosts: Read-only file system
+++ Updating remotes done...
+++ Creating local branch automated-cherry-pick-of-mattermost-#31201-upstream-release-10.7-1748857285
Switched to a new branch 'automated-cherry-pick-of-mattermost-#31201-upstream-release-10.7-1748857285'
Branch 'automated-cherry-pick-of-mattermost-#31201-upstream-release-10.7-1748857285' set up to track remote branch 'release-10.7' from 'upstream'.

+++ About to attempt cherry pick of PR #31201 with merge commit d38c27f96fcf754c36f231d1f2e9dbd48ad40bab.

Auto-merging server/channels/app/import.go
Auto-merging server/channels/app/import_functions.go
Auto-merging server/channels/app/import_test.go
CONFLICT (content): Merge conflict in server/channels/app/import_test.go
Auto-merging server/i18n/en.json
CONFLICT (content): Merge conflict in server/i18n/en.json
error: could not apply d38c27f96f... [MM-64402] Improve validation of imported attachments (#31201)
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".

+++ Conflicts detected:

UU server/channels/app/import_test.go
UU server/i18n/en.json
Aborting.

+++ Aborting in-progress git cherry-pick.

+++ Returning you to the master branch and cleaning up.

```


[MM-64402]: https://mattermost.atlassian.net/browse/MM-64402?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
author_association: CONTRIBUTOR
user: login: mattermost-build
created_at: 2025-06-02T09:41:37Z
body: Looks like you don't have permissions to trigger this command.
 Only available for the PR submitter and org members
author_association: CONTRIBUTOR
user: login: mattermost-build
created_at: 2025-06-02T09:41:39Z
body: Looks like you don't have permissions to trigger this command.
 Only available for the PR submitter and org members
author_association: CONTRIBUTOR
user: login: amyblais
created_at: 2025-06-02T09:42:21Z
body: @streamer45 Can you help with manual cherry-pick PRs for ``release-10.7``, ``release-10.6``, ``release-10.5`` and ``release-9.11`` branches?
author_association: MEMBER
user: login: streamer45
created_at: 2025-06-02T11:42:13Z
body: > @streamer45 Can you help with manual cherry-pick PRs for `release-10.7`, `release-10.6`, `release-10.5` and `release-9.11` branches?

Sure, hopefully it won't be a nightmare :p
author_association: CONTRIBUTOR