siyuan-note/siyuan · error
import path is not sub path of import dir
Error message
import path is not sub path of import dir
What it means
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'.
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.
Example fix
// before
fd.append("file", blob, file.name) // may contain "..\\..\evil.sy"
// after
const safeName = file.name.replace(/[^\w.\-]+/g, "_")
fd.append("file", blob, safeName) Defensive patterns
Strategy: validation
Validate before calling
const safe = name.replace(/[^\w.\-]+/g, "_"); if (safe.includes("..") ) throw new Error("unsafe filename")
fd.append("file", blob, safe) Type guard
function isSafeFilename(name) { return /^[\w.\-]+$/.test(name) && !name.startsWith(".") && !/[\\/]/.test(name) } Try / catch
try { await upload(fd) }
catch (e) { if (e.message.includes("not sub path")) notify("Filename rejected; rename the file") else throw e } Prevention
- 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
When it happens
Trigger: 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.
Common situations: Malicious uploads probing path traversal; filenames containing null bytes or separators on Windows; proxies rewriting the filename header.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- asset path escapes data directory
- asset path escapes data directory
- export path is outside export directory
- history path [ ] is not in workspace
- history path [ ] is not under history directory
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/18bc93c80a08a2a7.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/api/import.go:294
return false
}
}
return true
}
func saveImportUploadFile(c *gin.Context, file *multipart.FileHeader) (writePath string, cleanup func(), err error) {
if file == nil {
return "", nil, errors.New("no file found")
}
importDir := filepath.Join(util.TempDir, "import", gulu.Rand.String(7))
if err = os.MkdirAll(importDir, 0755); err != nil {
return
}
cleanup = func() { _ = os.RemoveAll(importDir) }
writePath = filepath.Join(importDir, filepath.Base(file.Filename))
if !gulu.File.IsSubPath(importDir, writePath) {
err = errors.New("import path is not sub path of import dir")
cleanup()
return
}
if err = c.SaveUploadedFile(file, writePath); err != nil {
cleanup()
}
return
}
var importData = contractHandler(apicontract.ImportData, func(c *gin.Context, request apicontract.ImportDataRequest) apicontract.Response[apicontract.Null] {
defer util.ClearPushProgress(100)
if request.File == nil {
return apicontract.Failure[apicontract.Null](-1, "file not found")
}
importDir := filepath.Join(util.TempDir, "import")
err := os.MkdirAll(importDir, 0755)
if err != nil {View on GitHub (pinned to 9f775e8a12)