siyuan-note/siyuan · error
notebook asset path resolves outside notebook directory
Error message
notebook asset path resolves outside notebook directory: %s
What it means
For notebook-level assets (assetDirIndex > 0), ResolveDataAssetPath evaluates the real paths of the data dir, the notebook root, and the assets root, then requires that the notebook root is inside the data dir and the assets root is inside the notebook root. This catches symlinks that redirect a notebook's assets directory outside its own notebook — a path-traversal / containment attack or an unintended link. When containment fails (or any real-path resolution errors), this error is thrown.
Solutions
- Replace the symlink with a real directory inside the notebook and copy the assets back
- Point the path at the actual location of the assets instead of through the symlink
- Verify the notebook directory itself exists and resolves inside the workspace data dir
- If containment was set up intentionally (shared assets), disable that setup — the resolver intentionally forbids it for security
Example fix
// before (assets is a symlink to /mnt/shared/assets) ln -s /mnt/shared/assets data/20240101120000-abc123/assets // after (real directory inside the notebook) mv /mnt/shared/assets/* data/20240101120000-abc123/assets/ rm data/20240101120000-abc123/assets-link && mkdir data/20240101120000-abc123/assets
Defensive patterns
Strategy: try-catch
Validate before calling
func notebookAssetsContained(dataDir, notebookID string) bool {
eval := func(p string) (string, error) { return filepath.EvalSymlinks(p) }
root, err1 := eval(filepath.Join(dataDir, notebookID, "assets"))
nb, err2 := eval(filepath.Join(dataDir, notebookID))
dd, err3 := eval(dataDir)
if err1 != nil || err2 != nil || err3 != nil {
return false
}
return strings.HasPrefix(root, nb+string(os.PathSeparator)) && strings.HasPrefix(nb, dd+string(os.PathSeparator))
} Try / catch
rel, abs, err := model.ResolveDataAssetPath(assetPath)
if err != nil {
if strings.Contains(err.Error(), "resolves outside notebook directory") {
return fmt.Errorf("refusing asset %q: symlink escapes notebook (remove the link)", assetPath)
}
return err
} Prevention
- Never symlink a notebook's assets directory outside the notebook
- Avoid sync/backup tools that replace directories with links
- Run the containment precheck (EvalSymlinks + prefix check) before batch asset operations
- Keep the notebook directory itself a real directory inside the workspace data dir
When it happens
Trigger: Calling ResolveDataAssetPath with `<notebookID>/assets/...` where `assets` (or the notebook dir itself) is a symlink pointing outside the notebook directory, or where the data dir / notebook root real-path resolution fails (deleted or broken path).
Common situations: Users symlinking a notebook's assets folder to another location (external drive, shared folder); sync tools replacing directories with links; deliberate path-traversal attempts against the API; workspace directories renamed while stale symlinks remain.
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
- symlink [ ] resolves outside data/assets: [ ]
- archive entry resolves outside destination
- asset path resolves outside assets directory
- boot appearance asset forbidden
- child template path is outside the current template package
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/1a5765286515aa8c.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/assets.go:1093
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 := ResolveAssetPathWithMissingLeaf(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 := ResolveRealPath(util.DataDir)
resolvedNotebookRoot, notebookEvalErr := ResolveRealPath(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 := ResolveAssetPathWithMissingLeaf(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 9f775e8a12)