fix: only match root-level JSONL files when importing a zip

When importing a Mattermost export zip, the code iterated over all files
to find the first .jsonl by extension. Exported attachments under data/
could themselves be .jsonl files, causing the import to pick an
attachment as the manifest instead of the actual root-level JSONL file.

Extract an IsRootJsonlFile helper in the imports package and use it in
the import process worker, mmctl validator, and bulk import test to
restrict the search to files with no directory component.
This commit is contained in:
Felipe Martin 2026-03-04 19:30:52 +01:00
parent 885ebdd4f1
commit ad7f230f06
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
5 changed files with 41 additions and 23 deletions

View file

@ -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
}

View file

@ -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)

View file

@ -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{

View file

@ -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)

View file

@ -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")