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

  1. Send a plain, simple filename (no path separators, no '..') in the multipart part.
  2. Sanitize the filename client-side before upload: keep alphanumerics/dot/extension only.
  3. On the server this is a correct rejection — fix the request rather than bypassing the check; log and reject with 400.
  4. 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

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


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)