siyuan-note/siyuan · error

notebook asset path resolves outside notebook directory: %s

Error message

notebook asset path resolves outside notebook directory: %s

What it means

Thrown by ResolveDataAssetPath (kernel/model/assets.go:961) only for notebook-relative asset paths (assetDirIndex > 0). After resolving symlinks, it verifies that the resolved notebook root still sits inside the resolved DataDir AND that the resolved assets root sits inside the resolved notebook root. If either chain is broken — typically because a symlink inside the notebook escapes the notebook — the request is rejected. This is a path-traversal / symlink-escape security guard, exercised by TestResolveDataAssetPath.

Source

Thrown at kernel/model/assets.go:961

	assetRoot := filepath.Join(util.DataDir, filepath.FromSlash(strings.Join(assetRootParts, "/")))
	if !gulu.File.IsSubPath(assetRoot, absPath) {
		err = fmt.Errorf("path is not a child of assets directory: %s", assetPath)
		return
	}

	resolvedRoot, evalErr := filepath.EvalSymlinks(assetRoot)
	if evalErr != nil {
		err = fmt.Errorf("resolve assets directory [%s] failed: %w", assetRoot, evalErr)
		return
	}
	if assetDirIndex > 0 {
		notebookRoot := filepath.Join(util.DataDir, parts[0])
		resolvedDataDir, dataEvalErr := filepath.EvalSymlinks(util.DataDir)
		resolvedNotebookRoot, notebookEvalErr := filepath.EvalSymlinks(notebookRoot)
		if dataEvalErr != nil || notebookEvalErr != nil ||
			!gulu.File.IsSubPath(resolvedDataDir, resolvedNotebookRoot) ||
			!gulu.File.IsSubPath(resolvedNotebookRoot, resolvedRoot) {
			err = fmt.Errorf("notebook asset path resolves outside notebook directory: %s", assetPath)
			return
		}
	}
	resolvedPath, evalErr := filepath.EvalSymlinks(absPath)
	if evalErr != nil {
		err = fmt.Errorf("resolve asset [%s] failed: %w", absPath, evalErr)
		return
	}
	if !gulu.File.IsSubPath(resolvedRoot, resolvedPath) {
		err = fmt.Errorf("asset path resolves outside assets directory: %s", assetPath)
		return
	}

	relativePath = filepath.ToSlash(dataRelativePath)
	return
}

// ResolveUnusedDataAssetPath 解析 data 相对资源路径,并确认目标当前未被引用。

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Audit symlinks inside the offending notebook: `find <DataDir>/<boxID> -type l -ls` and identify which link resolves outside the notebook.
  2. Replace the escaping symlink with either a real copy of the asset or a relative symlink that stays inside the notebook directory.
  3. If this is unexpected, treat it as a security event: the input may come from an untrusted document, so reject the document/asset rather than weakening the check.
  4. Do not attempt to bypass by editing the guard; instead store the asset under the notebook's own assets/ directory.
Defensive patterns

Strategy: validation

Validate before calling

// Reject asset paths whose notebook subpath traverses a symlink out of the notebook.
func notebookSelfContained(boxDir, assetRel string) bool {
    full := filepath.Join(boxDir, filepath.FromSlash(assetRel))
    real, err := filepath.EvalSymlinks(full)
    if err != nil { return false }
    realBox, err := filepath.EvalSymlinks(boxDir)
    if err != nil { return false }
    return gulu.File.IsSubPath(realBox, real)
}

Try / catch

if _, _, err := model.ResolveDataAssetPath(p); err != nil && strings.Contains(err.Error(), "resolves outside notebook") {
    // a symlink inside the notebook escapes the notebook; do not attempt to follow it manually
    logging.LogWarningf("rejected escaping notebook symlink for asset: %s", p)
}

Prevention

When it happens

Trigger: Calling ResolveDataAssetPath with a path like `<boxID>/linked/assets/file.png` where `<boxID>/linked` is a symlink to a directory outside `<boxID>/`, so the resolved assets root no longer lives under the notebook root. Also fires if the notebook directory itself is a symlink that resolves outside DataDir.

Common situations: A user (or a maliciously crafted document) places a symlink inside a notebook that points elsewhere on disk to expose or overwrite files; a workspace that was restructured with symlinks for compatibility; migrating a notebook by symlinking its old location that lives outside the new DataDir.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/1a5765286515aa8c. Report an issue: GitHub.