{"record":{"id":"18bc93c80a08a2a7","repo":"siyuan-note/siyuan","slug":"import-path-is-not-sub-path-of-import-dir","errorCode":null,"errorMessage":"import path is not sub path of import dir","messagePattern":"import path is not sub path of import dir","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/api/import.go","lineNumber":331,"sourceCode":"func saveImportUpload(c *gin.Context) (form *multipart.Form, writePath string, cleanup func(), err error) {\n\tform, err = c.MultipartForm()\n\tif err != nil {\n\t\treturn\n\t}\n\tfiles := form.File[\"file\"]\n\tif len(files) < 1 {\n\t\terr = errors.New(\"no file found\")\n\t\treturn\n\t}\n\n\timportDir := filepath.Join(util.TempDir, \"import\", gulu.Rand.String(7))\n\tif err = os.MkdirAll(importDir, 0755); err != nil {\n\t\treturn\n\t}\n\tcleanup = func() { _ = os.RemoveAll(importDir) }\n\twritePath = filepath.Join(importDir, filepath.Base(files[0].Filename))\n\tif !gulu.File.IsSubPath(importDir, writePath) {\n\t\terr = errors.New(\"import path is not sub path of import dir\")\n\t\tcleanup()\n\t\treturn\n\t}\n\n\tif err = c.SaveUploadedFile(files[0], writePath); err != nil {\n\t\tcleanup()\n\t}\n\treturn\n}\n\nfunc importData(c *gin.Context) {\n\tret := gulu.Ret.NewResult()\n\tdefer c.JSON(http.StatusOK, ret)\n\n\tutil.PushEndlessProgress(model.Conf.Language(73))\n\tdefer util.ClearPushProgress(100)\n\n\tform, err := c.MultipartForm()","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/api/import.go#L313-L349","documentation":"Returned by saveImportUpload (import.go:331) when, after taking filepath.Base(files[0].Filename) and joining it with the random importDir, the resulting writePath is NOT a subpath of importDir. This is a path-traversal guard: a filename containing path separators or '..' could escape the temp import directory. gulu.File.IsSubPath(importDir, writePath) fails, so the upload is refused and the temp dir cleaned up.","triggerScenarios":"Uploading a file whose Filename (content-disposition) contains directory traversal sequences like '../escape.zip', absolute paths like '/etc/x', or backslashes that filepath.Base does not fully neutralize on the running OS. The check at import.go:330 fails and cleanup() runs at line 332.","commonSituations":"Malicious or malformed client sending a crafted filename. A reverse-proxy or middleware rewriting the filename. Edge cases where filepath.Base returns a segment that still resolves outside importDir on the host OS (e.g. Windows drive letters, UNC paths).","solutions":["Sanitize the uploaded filename client-side to a simple basename with no separators or '..' before sending.","On the server side of your integration, generate a safe random filename (e.g. gulu.Rand.String) instead of trusting the user-supplied name.","If you control the client, set the FormData filename to a plain slug like 'import.zip'."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Strip traversal/separator chars from the filename before upload\nconst safeName = uploadedFile.name.replace(/[^A-Za-z0-9._-]/g, '_');\nfd.append('file', blob, safeName);","typeGuard":null,"tryCatchPattern":"try { await importUpload(fd); }\ncatch (e) { if (/not sub path/.test(e.msg)) { fd.set('file', blob, 'import.zip'); await importUpload(fd); } else throw e; }","preventionTips":["Always sanitize uploaded filenames to a plain basename.","Prefer generating a random server-side filename over trusting client input.","Reject filenames containing path separators or '..' client-side."],"tags":["import","security","validation","filesystem","kernel"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}