siyuan-note/siyuan · warning

invalid export path

Error message

invalid export path

What it means

`AcquireExportArtifactLease` validates the requested export file name after URL-decoding and cleaning. If the cleaned name is `.`, starts with `..`, or is an absolute path, the request is a path-traversal attempt and is rejected with 'invalid export path'.

Source

Thrown at kernel/model/encrypted_export.go:186

// normalExportTempName 避免普通导出使用加密导出的第一层归属目录,保留归档内部的原始名称。
func normalExportTempName(name string) string {
	parts := strings.SplitN(filepath.ToSlash(filepath.Clean(name)), "/", 2)
	if ast.IsNodeIDPattern(parts[0]) {
		parts[0] = "export-" + parts[0]
	}
	return filepath.FromSlash(strings.Join(parts, "/"))
}

// AcquireExportArtifactLease 为导出产物取得覆盖整个复制过程的生命周期租约。
func AcquireExportArtifactLease(exportPath string) (lease *ExportArtifactLease, err error) {
	if after, ok := strings.CutPrefix(exportPath, "/export/"); ok {
		fileName, decodeErr := url.PathUnescape(after)
		if decodeErr != nil {
			return nil, decodeErr
		}
		fileName = filepath.Clean(fileName)
		if fileName == "." || strings.HasPrefix(fileName, "..") || filepath.IsAbs(fileName) {
			return nil, errors.New("invalid export path")
		}
		if IsManagedEncryptedExportPath(fileName) {
			boxID, artifact, resolved := ResolveManagedEncryptedExport(fileName)
			if !resolved {
				return nil, errors.New("managed export is unavailable")
			}
			if err = AcquireEncryptedBoxOperation(boxID); err != nil {
				return nil, err
			}
			HoldBoxReadLock(boxID)
			release := true
			defer func() {
				if release {
					ReleaseBoxReadLock(boxID)
					ReleaseEncryptedBoxOperation(boxID)
				}
			}()
			_, artifact, resolved = ResolveManagedEncryptedExport(fileName)

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Pass only a plain file name (no directories, no `..`, not absolute) to the export lease API.
  2. URL-encode file names correctly on the client; do not pre-decode them before sending.
  3. Use a managed export path (via IsManagedEncryptedExportPath flow) returned by a previous export call instead of constructing paths manually.
  4. If a legitimate file is rejected, check whether the name contains traversal segments after filepath.Clean.

Example fix

// before
await fetchPost("/api/export/acquireLease", {name: "../../tmp/export/report.html"})
// after
await fetchPost("/api/export/acquireLease", {name: "report.html"})
Defensive patterns

Strategy: validation

Validate before calling

// Go: client-side guard before requesting an export lease
func safeExportName(name string) bool {
    c := filepath.Clean(name)
    return c != "." && !strings.HasPrefix(c, "..") && !filepath.IsAbs(c)
}

Prevention

When it happens

Trigger: Requesting a mobile/managed export artifact lease with a `name`/path parameter that escapes the export directory — e.g. `..%2F..%2Fsecret.txt`, an absolute path like `/etc/passwd`, or a bare `.`.

Common situations: Buggy client code passing a full path instead of a file name; stale UI links referencing moved files with relative segments; malicious probing of the export endpoint.

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@8641553a1f (2026-09-11). Data as JSON: /api/errors/6f2821c8e352456f. Report an issue: GitHub.