diff --git a/server/channels/app/import_test.go b/server/channels/app/import_test.go index e7ee9e09fbe..6f2adb9d070 100644 --- a/server/channels/app/import_test.go +++ b/server/channels/app/import_test.go @@ -650,7 +650,7 @@ func TestImportBulkImportWithAttachments(t *testing.T) { var jsonFile io.ReadCloser for _, f := range importZipReader.File { - if filepath.Ext(f.Name) != ".jsonl" { + if !imports.IsRootJsonlFile(f.Name) { continue } diff --git a/server/channels/app/imports/import_validators.go b/server/channels/app/imports/import_validators.go index fac52335d63..7aee8378943 100644 --- a/server/channels/app/imports/import_validators.go +++ b/server/channels/app/imports/import_validators.go @@ -16,6 +16,14 @@ import ( "github.com/mattermost/mattermost/server/public/shared/mlog" ) +// IsRootJsonlFile reports whether the given zip entry name refers to a .jsonl +// file at the root of the archive (no directory component). This is used to +// locate the import manifest while ignoring .jsonl files that may exist as +// exported attachments in subdirectories. +func IsRootJsonlFile(name string) bool { + return filepath.Ext(name) == ".jsonl" && filepath.Dir(name) == "." +} + func ValidateSchemeImportData(data *SchemeImportData) *model.AppError { if data.Scope == nil { return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.null_scope.error", nil, "", http.StatusBadRequest) diff --git a/server/channels/app/imports/import_validators_test.go b/server/channels/app/imports/import_validators_test.go index a046d0db4a5..09ce78b906c 100644 --- a/server/channels/app/imports/import_validators_test.go +++ b/server/channels/app/imports/import_validators_test.go @@ -16,6 +16,18 @@ import ( "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" ) +func TestIsRootJsonlFile(t *testing.T) { + assert.True(t, IsRootJsonlFile("import.jsonl")) + assert.True(t, IsRootJsonlFile("export.jsonl")) + assert.False(t, IsRootJsonlFile("data/export.jsonl")) + assert.False(t, IsRootJsonlFile("data/attachments/report.jsonl")) + assert.False(t, IsRootJsonlFile("data/deep/nested/file.jsonl")) + assert.False(t, IsRootJsonlFile("import.json")) + assert.False(t, IsRootJsonlFile("import.zip")) + assert.False(t, IsRootJsonlFile("data/photo.jpg")) + assert.False(t, IsRootJsonlFile("")) +} + func TestImportValidateSchemeImportData(t *testing.T) { // Test with minimum required valid properties and team scope. data := SchemeImportData{ diff --git a/server/channels/jobs/import_process/worker.go b/server/channels/jobs/import_process/worker.go index 1202f783dda..f9e974b8405 100644 --- a/server/channels/jobs/import_process/worker.go +++ b/server/channels/jobs/import_process/worker.go @@ -19,6 +19,7 @@ import ( "github.com/mattermost/mattermost/server/public/shared/configservice" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/request" + "github.com/mattermost/mattermost/server/v8/channels/app/imports" "github.com/mattermost/mattermost/server/v8/channels/jobs" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" ) @@ -100,29 +101,28 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) *jobs.SimpleWorker { } // find JSONL import file. - var jsonFile io.ReadCloser + var jsonZipFile *zip.File for _, f := range importZipReader.File { - if filepath.Ext(f.Name) != ".jsonl" { - continue + if imports.IsRootJsonlFile(f.Name) { + jsonZipFile = f + break } - // avoid "zip slip" - if strings.Contains(f.Name, "..") { - return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "jsonFilePath contains path traversal", http.StatusForbidden) - } - - jsonFile, err = f.Open() - if err != nil { - return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "", http.StatusInternalServerError).Wrap(err) - } - - defer jsonFile.Close() - break } - - if jsonFile == nil { + if jsonZipFile == nil { return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.missing_jsonl", nil, "jsonFile was nil", http.StatusBadRequest) } + // avoid "zip slip" + if strings.Contains(jsonZipFile.Name, "..") { + return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "jsonFilePath contains path traversal", http.StatusForbidden) + } + + jsonFile, err := jsonZipFile.Open() + if err != nil { + return model.NewAppError("ImportProcessWorker", "import_process.worker.do_job.open_file", nil, "", http.StatusInternalServerError).Wrap(err) + } + defer jsonFile.Close() + extractContent := job.Data["extract_content"] == "true" // do the actual import. lineNumber, appErr := app.BulkImportWithPath(appContext, jsonFile, importZipReader, false, extractContent, runtime.NumCPU(), model.ExportDataDir) diff --git a/server/cmd/mmctl/commands/importer/validate.go b/server/cmd/mmctl/commands/importer/validate.go index e8192b70974..64d1b85f9fa 100644 --- a/server/cmd/mmctl/commands/importer/validate.go +++ b/server/cmd/mmctl/commands/importer/validate.go @@ -248,12 +248,10 @@ func (v *Validator) Validate() error { var jsonlZip *zip.File for _, zfile := range z.File { - if filepath.Ext(zfile.Name) != ".jsonl" { - continue + if imports.IsRootJsonlFile(zfile.Name) { + jsonlZip = zfile + break } - - jsonlZip = zfile - break } if jsonlZip == nil { return fmt.Errorf("could not find a .jsonl file in the import archive")