{"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":294,"sourceCode":"\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc saveImportUploadFile(c *gin.Context, file *multipart.FileHeader) (writePath string, cleanup func(), err error) {\n\tif file == nil {\n\t\treturn \"\", nil, errors.New(\"no file found\")\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(file.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(file, writePath); err != nil {\n\t\tcleanup()\n\t}\n\treturn\n}\n\nvar importData = contractHandler(apicontract.ImportData, func(c *gin.Context, request apicontract.ImportDataRequest) apicontract.Response[apicontract.Null] {\n\tdefer util.ClearPushProgress(100)\n\tif request.File == nil {\n\t\treturn apicontract.Failure[apicontract.Null](-1, \"file not found\")\n\t}\n\timportDir := filepath.Join(util.TempDir, \"import\")\n\terr := os.MkdirAll(importDir, 0755)\n\tif err != nil {","sourceCodeStart":276,"sourceCodeEnd":312,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/api/import.go#L276-L312","documentation":"saveImportUploadFile builds writePath from filepath.Base(file.Filename) inside a fresh temp import dir, then double-checks with gulu.File.IsSubPath(importDir, writePath). If the computed write path escapes the import dir (defense-in-depth against traversal via odd filenames), it deletes the temp dir and fails with 'import path is not sub path of import dir'.","triggerScenarios":"An upload whose filename, after Base() and Join normalization, resolves outside importDir — e.g. crafted filenames like '..%2f..' surviving normalization, or symlinks/abs-path filenames on unusual platforms.","commonSituations":"Malicious uploads probing path traversal; filenames containing null bytes or separators on Windows; proxies rewriting the filename header.","solutions":["Send a plain, simple filename (no path separators, no '..') in the multipart part.","Sanitize the filename client-side before upload: keep alphanumerics/dot/extension only.","On the server this is a correct rejection — fix the request rather than bypassing the check; log and reject with 400.","Test the exact failing filename in isolation to see why Join/Base normalization escapes the dir."],"exampleFix":"// before\nfd.append(\"file\", blob, file.name) // may contain \"..\\\\..\\evil.sy\"\n\n// after\nconst safeName = file.name.replace(/[^\\w.\\-]+/g, \"_\")\nfd.append(\"file\", blob, safeName)","handlingStrategy":"validation","validationCode":"const safe = name.replace(/[^\\w.\\-]+/g, \"_\"); if (safe.includes(\"..\") ) throw new Error(\"unsafe filename\")\nfd.append(\"file\", blob, safe)","typeGuard":"function isSafeFilename(name) { return /^[\\w.\\-]+$/.test(name) && !name.startsWith(\".\") && !/[\\\\/]/.test(name) }","tryCatchPattern":"try { await upload(fd) }\ncatch (e) { if (e.message.includes(\"not sub path\")) notify(\"Filename rejected; rename the file\") else throw e }","preventionTips":["Sanitize filenames before upload; strip path separators and '..'","Never trust filename headers from external sources","Keep the server-side IsSubPath check intact — treat hits as malicious or buggy clients"],"tags":["import","security","path-traversal","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}