[MM-64402] Improve validation of imported attachments (#31201)

* Improve validation of imported attachments

* Simplify multiple errors handling

* Improve logic

* Fix abs paths in tests

* Remove redundant clean

* Implement additional validation

* Fix absolute paths in test

* Add additional tests

---------

Co-authored-by: Lorenzo Gallegos <1328683+enzowritescode@users.noreply.github.com>
Этот коммит содержится в:
Claudio Costa
2025-05-28 23:44:50 -06:00
коммит произвёл GitHub
родитель 07239a5217
Коммит d38c27f96f
7 изменённых файлов: 773 добавлений и 32 удалений

Просмотреть файл

@@ -7,6 +7,7 @@ import (
"archive/zip" "archive/zip"
"bufio" "bufio"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -45,20 +46,32 @@ func processAttachmentPaths(c request.CTX, files *[]imports.AttachmentImportData
if files == nil { if files == nil {
return nil return nil
} }
var ok bool var ok bool
var errs []error
for i, f := range *files { for i, f := range *files {
if f.Path != nil { if f.Path != nil {
path := filepath.Join(basePath, *f.Path) originalPath := *f.Path
path, valid := imports.ValidateAttachmentPathForImport(originalPath, basePath)
*f.Path = path *f.Path = path
if !valid {
errs = append(errs, fmt.Errorf("invalid attachment path %q", originalPath))
continue
}
if len(filesMap) > 0 { if len(filesMap) > 0 {
if (*files)[i].Data, ok = filesMap[path]; !ok { if (*files)[i].Data, ok = filesMap[*f.Path]; !ok {
return fmt.Errorf("attachment %q not found in map", path) 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 { 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": case "user":
if line.User.ProfileImage != nil { 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 *line.User.ProfileImage = path
if len(filesMap) > 0 { if len(filesMap) > 0 {
if line.User.ProfileImageData, ok = filesMap[path]; !ok { if line.User.ProfileImageData, ok = filesMap[path]; !ok {
@@ -98,7 +115,11 @@ func processAttachments(c request.CTX, line *imports.LineImportData, basePath st
} }
case "bot": case "bot":
if line.Bot.ProfileImage != nil { 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 *line.Bot.ProfileImage = path
if len(filesMap) > 0 { if len(filesMap) > 0 {
if line.Bot.ProfileImageData, ok = filesMap[path]; !ok { if line.Bot.ProfileImageData, ok = filesMap[path]; !ok {
@@ -108,7 +129,11 @@ func processAttachments(c request.CTX, line *imports.LineImportData, basePath st
} }
case "emoji": case "emoji":
if line.Emoji.Image != nil { 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 *line.Emoji.Image = path
if len(filesMap) > 0 { if len(filesMap) > 0 {
if line.Emoji.Data, ok = filesMap[path]; !ok { if line.Emoji.Data, ok = filesMap[path]; !ok {

Просмотреть файл

@@ -193,7 +193,6 @@ func (a *App) importTeam(rctx request.CTX, data *imports.TeamImportData, dryRun
var team *model.Team var team *model.Team
team, err := a.Srv().Store().Team().GetByName(teamName) team, err := a.Srv().Store().Team().GetByName(teamName)
if err != nil { if err != nil {
team = &model.Team{ team = &model.Team{
Name: teamName, 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 { func (a *App) importDirectChannel(rctx request.CTX, data *imports.DirectChannelImportData, dryRun bool) *model.AppError {
var err *model.AppError var err *model.AppError
if err = imports.ValidateDirectChannelImportData(data); err != nil { 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) 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 var page int
for int64(len(ems)) < totalMembers { for int64(len(ems)) < totalMembers {

Просмотреть файл

@@ -157,7 +157,7 @@ func TestImportBulkImport(t *testing.T) {
username3 := model.NewUsername() username3 := model.NewUsername()
emojiName := model.NewId() emojiName := model.NewId()
testsDir, _ := fileutils.FindDir("tests") 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\"}` 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\"}` 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": "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 + `"}}` {"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.Nil(t, err, "BulkImport should have succeeded")
require.Equal(t, 0, line, "BulkImport line should be 0") require.Equal(t, 0, line, "BulkImport line should be 0")
// Run bulk import using a string that contains a line with invalid json. // Run bulk import using a string that contains a line with invalid json.
data2 := `{"type": "version", "version": 1` 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.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.") 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": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}}
{"type": "user", "user": {"username": "kufjgnkxkrhhfgbrip6qxkfsaa", "email": "kufjgnkxkrhhfgbrip6qxkfsaa@example.com"}} {"type": "user", "user": {"username": "kufjgnkxkrhhfgbrip6qxkfsaa", "email": "kufjgnkxkrhhfgbrip6qxkfsaa@example.com"}}
{"type": "user", "user": {"username": "bwshaim6qnc2ne7oqkd5b2s2rq", "email": "bwshaim6qnc2ne7oqkd5b2s2rq@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}}` {"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.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.") 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.Nil(t, err, "BulkImport should have succeeded")
require.Equal(t, 0, line, "BulkImport line should be 0") 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) { 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) { func TestProcessAttachments(t *testing.T) {
c := request.TestContext(t) c := request.TestContext(t)
@@ -340,35 +515,35 @@ func TestProcessAttachments(t *testing.T) {
t.Run("valid path", func(t *testing.T) { t.Run("valid path", func(t *testing.T) {
expected := &[]imports.AttachmentImportData{ 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) { 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.NoError(t, err)
require.Equal(t, expected, line.Post.Attachments) require.Equal(t, expected, line.Post.Attachments)
}) })
t.Run("direct post attachments", func(t *testing.T) { 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.NoError(t, err)
require.Equal(t, expected, line2.DirectPost.Attachments) require.Equal(t, expected, line2.DirectPost.Attachments)
}) })
t.Run("profile image", func(t *testing.T) { t.Run("profile image", func(t *testing.T) {
expected := "/tmp/profile.jpg" expected := "tmp/profile.jpg"
err := processAttachments(c, &userLine, "/tmp", nil) err := processAttachments(c, &userLine, "tmp", nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, *userLine.User.ProfileImage) require.Equal(t, expected, *userLine.User.ProfileImage)
}) })
t.Run("emoji", func(t *testing.T) { t.Run("emoji", func(t *testing.T) {
expected := "/tmp/emoji.png" expected := "tmp/emoji.png"
err := processAttachments(c, &emojiLine, "/tmp", nil) err := processAttachments(c, &emojiLine, "tmp", nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expected, *emojiLine.Emoji.Image) 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("with filesMap", func(t *testing.T) {
t.Run("post attachments", func(t *testing.T) { t.Run("post attachments", func(t *testing.T) {
filesMap := map[string]*zip.File{ filesMap := map[string]*zip.File{
"/tmp/file.jpg": nil, "tmp/file.jpg": nil,
} }
err := processAttachments(c, &line, "", filesMap) err := processAttachments(c, &line, "", filesMap)
require.Error(t, err) require.Error(t, err)
filesMap["/tmp/somedir/file.jpg"] = nil filesMap["tmp/somedir/file.jpg"] = nil
err = processAttachments(c, &line, "", filesMap) err = processAttachments(c, &line, "", filesMap)
require.NoError(t, err) require.NoError(t, err)
}) })
t.Run("direct post attachments", func(t *testing.T) { t.Run("direct post attachments", func(t *testing.T) {
filesMap := map[string]*zip.File{ filesMap := map[string]*zip.File{
"/tmp/file.jpg": nil, "tmp/file.jpg": nil,
} }
err := processAttachments(c, &line2, "", filesMap) err := processAttachments(c, &line2, "", filesMap)
require.Error(t, err) require.Error(t, err)
filesMap["/tmp/somedir/file.jpg"] = nil filesMap["tmp/somedir/file.jpg"] = nil
err = processAttachments(c, &line2, "", filesMap) err = processAttachments(c, &line2, "", filesMap)
require.NoError(t, err) require.NoError(t, err)
}) })
@@ -406,7 +581,7 @@ func TestProcessAttachments(t *testing.T) {
err := processAttachments(c, &userLine, "", filesMap) err := processAttachments(c, &userLine, "", filesMap)
require.Error(t, err) require.Error(t, err)
filesMap["/tmp/profile.jpg"] = nil filesMap["tmp/profile.jpg"] = nil
err = processAttachments(c, &userLine, "", filesMap) err = processAttachments(c, &userLine, "", filesMap)
require.NoError(t, err) require.NoError(t, err)
}) })
@@ -418,7 +593,7 @@ func TestProcessAttachments(t *testing.T) {
err := processAttachments(c, &emojiLine, "", filesMap) err := processAttachments(c, &emojiLine, "", filesMap)
require.Error(t, err) require.Error(t, err)
filesMap["/tmp/emoji.png"] = nil filesMap["tmp/emoji.png"] = nil
err = processAttachments(c, &emojiLine, "", filesMap) err = processAttachments(c, &emojiLine, "", filesMap)
require.NoError(t, err) require.NoError(t, err)
}) })

Просмотреть файл

@@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@@ -191,6 +192,10 @@ func ValidateChannelImportData(data *ChannelImportData) *model.AppError {
func ValidateUserImportData(data *UserImportData) *model.AppError { func ValidateUserImportData(data *UserImportData) *model.AppError {
if data.ProfileImage != nil && data.ProfileImageData == nil { 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) { 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) return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusNotFound).Wrap(err)
} else if err != nil { } else if err != nil {
@@ -320,6 +325,11 @@ func ValidateUserImportData(data *UserImportData) *model.AppError {
func ValidateBotImportData(data *BotImportData) *model.AppError { func ValidateBotImportData(data *BotImportData) *model.AppError {
if data.ProfileImage != nil && data.ProfileImageData == nil { 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) { 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) return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusNotFound).Wrap(err)
} else if err != nil { } 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 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) 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 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 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) 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 { if err := model.IsValidEmojiName(*data.Name); err != nil {
return err return err
} }
@@ -749,3 +788,42 @@ func isValidGuestRoles(data UserImportData) bool {
return true 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
}

Просмотреть файл

@@ -499,6 +499,12 @@ func TestImportValidateUserImportData(t *testing.T) {
err = ValidateUserImportData(&data) err = ValidateUserImportData(&data)
require.NotNil(t, err, "Validation should have failed due to not existing profile image file.") 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 data.ProfileImage = nil
// Invalid Emails // Invalid Emails
@@ -623,8 +629,8 @@ func TestImportValidateUserImportData(t *testing.T) {
data.NotifyProps.MentionKeys = model.NewPointer("valid") data.NotifyProps.MentionKeys = model.NewPointer("valid")
checkNoError(t, ValidateUserImportData(&data)) checkNoError(t, ValidateUserImportData(&data))
//Test the email batching interval validators // Test the email batching interval validators
//Happy paths // Happy paths
data.EmailInterval = model.NewPointer("immediately") data.EmailInterval = model.NewPointer("immediately")
checkNoError(t, ValidateUserImportData(&data)) checkNoError(t, ValidateUserImportData(&data))
@@ -634,7 +640,7 @@ func TestImportValidateUserImportData(t *testing.T) {
data.EmailInterval = model.NewPointer("hour") data.EmailInterval = model.NewPointer("hour")
checkNoError(t, ValidateUserImportData(&data)) checkNoError(t, ValidateUserImportData(&data))
//Invalid values // Invalid values
data.EmailInterval = model.NewPointer("invalid") data.EmailInterval = model.NewPointer("invalid")
checkError(t, ValidateUserImportData(&data)) checkError(t, ValidateUserImportData(&data))
@@ -731,6 +737,12 @@ func TestImportValidateBotImportData(t *testing.T) {
data.Owner = model.NewPointer(strings.Repeat("abcdefghij", 7)) data.Owner = model.NewPointer(strings.Repeat("abcdefghij", 7))
err = ValidateBotImportData(&data) err = ValidateBotImportData(&data)
require.NotNil(t, err, "Should have failed due to too long OwnerID.") 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) { func TestImportValidateUserTeamsImportData(t *testing.T) {
@@ -943,6 +955,21 @@ func TestImportValidateReplyImportData(t *testing.T) {
} }
err = ValidateReplyImportData(&data, parentCreateAt, maxPostSize) err = ValidateReplyImportData(&data, parentCreateAt, maxPostSize)
require.NotNil(t, err, "Should have failed due to 0 create-at value.") 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) { 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.") 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") 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) { func TestImportValidateDirectChannelImportData(t *testing.T) {
@@ -1438,10 +1483,29 @@ func TestImportValidateDirectPostImportData(t *testing.T) {
err = ValidateDirectPostImportData(&data, maxPostSize) err = ValidateDirectPostImportData(&data, maxPostSize)
require.Nil(t, err, "Validation should succeed with valid optional parameters") 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) { func TestImportValidateEmojiImportData(t *testing.T) {
var testCases = []struct { testCases := []struct {
testName string testName string
name *string name *string
image *string image *string
@@ -1456,6 +1520,7 @@ func TestImportValidateEmojiImportData(t *testing.T) {
{"nil name", nil, model.NewPointer("/path/to/image"), true, false}, {"nil name", nil, model.NewPointer("/path/to/image"), true, false},
{"nil image", model.NewPointer("parrot2"), nil, true, false}, {"nil image", model.NewPointer("parrot2"), nil, true, false},
{"nil name and image", nil, 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 { for _, tc := range testCases {
@@ -1485,7 +1550,7 @@ func checkNoError(t *testing.T, err *model.AppError) {
} }
func TestIsValidGuestRoles(t *testing.T) { func TestIsValidGuestRoles(t *testing.T) {
var testCases = []struct { testCases := []struct {
name string name string
input UserImportData input UserImportData
expected bool 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")
}
})
}
}

Просмотреть файл

@@ -475,4 +475,64 @@ func (s *MmctlUnitTestSuite) TestImportValidateCmdF() {
s.Require().Empty(res.Errors) s.Require().Empty(res.Errors)
s.Equal("Validation complete\n", printer.GetLines()[2]) 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])
})
} }

Просмотреть файл

@@ -5614,6 +5614,10 @@
"id": "app.import.profile_image.read_data.app_error", "id": "app.import.profile_image.read_data.app_error",
"translation": "Failed to read profile image data." "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", "id": "app.import.validate_bot_import_data.owner_missing.error",
"translation": "Bot owner is missing" "translation": "Bot owner is missing"
@@ -5678,6 +5682,10 @@
"id": "app.import.validate_direct_channel_import_data.unknown_favoriter.error", "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." "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", "id": "app.import.validate_direct_post_import_data.channel_members_required.error",
"translation": "Missing required direct post property: channel_members" "translation": "Missing required direct post property: channel_members"
@@ -5722,10 +5730,18 @@
"id": "app.import.validate_emoji_import_data.image_missing.error", "id": "app.import.validate_emoji_import_data.image_missing.error",
"translation": "Import emoji image field missing or blank." "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", "id": "app.import.validate_emoji_import_data.name_missing.error",
"translation": "Import emoji name field missing or blank." "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", "id": "app.import.validate_post_import_data.channel_missing.error",
"translation": "Missing required Post property: Channel." "translation": "Missing required Post property: Channel."
@@ -5782,6 +5798,10 @@
"id": "app.import.validate_reaction_import_data.user_missing.error", "id": "app.import.validate_reaction_import_data.user_missing.error",
"translation": "Missing required Reaction property: User." "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", "id": "app.import.validate_reply_import_data.create_at_missing.error",
"translation": "Missing required Reply property: create_at." "translation": "Missing required Reply property: create_at."
@@ -5946,6 +5966,10 @@
"id": "app.import.validate_user_import_data.guest_roles_conflict.error", "id": "app.import.validate_user_import_data.guest_roles_conflict.error",
"translation": "User roles are not consistent with guest status." "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", "id": "app.import.validate_user_import_data.last_name_length.error",
"translation": "User Last Name is too long." "translation": "User Last Name is too long."