siyuan-note/siyuan · error
asset path resolves outside notebook assets directory
Error message
asset path resolves outside notebook assets directory
What it means
After resolving a deferred asset path to a real path, deferredAssetPathFromFiles double-checks containment: the resolved file must be inside the data directory, inside the owning notebook's root, and inside the notebook's assets directory (checks via ResolveRealPath and IsSubPath). If any containment check or resolution fails, it returns 'asset path resolves outside notebook assets directory'. Unlike the escape check (604), this catches paths that pass lexical normalization but escape via symlinks or land in another notebook's tree.
Source
Thrown at kernel/model/asset_download_read.go:112
return "", errors.New("asset path escapes data directory")
}
if boxID == "" && !IsEncryptedAssetPath(absPath) {
if _, _, resolveErr := ResolveDataAssetPath(lookupPath); resolveErr != nil {
return "", resolveErr
}
} else {
resolvedBoxID := boxID
if resolvedBoxID == "" {
resolvedBoxID = ExtractBoxIDFromAssetsPath(absPath)
}
root, rootErr := ResolveAssetPathWithMissingLeaf(filepath.Join(util.DataDir, resolvedBoxID, "assets"))
resolved, resolveErr := ResolveAssetPathWithMissingLeaf(absPath)
notebookRoot, notebookErr := ResolveRealPath(filepath.Join(util.DataDir, resolvedBoxID))
dataRoot, dataErr := ResolveRealPath(util.DataDir)
if rootErr != nil || resolveErr != nil || notebookErr != nil || dataErr != nil ||
!gulu.File.IsSubPath(dataRoot, notebookRoot) ||
!gulu.File.IsSubPath(notebookRoot, root) || !gulu.File.IsSubPath(root, resolved) {
return "", errors.New("asset path resolves outside notebook assets directory")
}
}
if lookupPath == relativePath {
return absPath, nil
}
candidates = append(candidates, absPath)
}
sort.Strings(candidates)
if len(candidates) > 0 {
return candidates[0], nil
}
return "", nil
}
// ensureReadableAssetLocal 先检查加密笔记本准入,再下载原始密文;实际读取仍须认证解密。
func ensureReadableAssetLocal(absPath string) error {
boxID := ExtractBoxIDFromAssetsPath(absPath)
if boxID != "" && IsEncryptedBox(boxID) && !IsBoxUnlocked(boxID) {View on GitHub (pinned to 8641553a1f)
Solutions
- Replace cross-notebook symlinks with real files inside the referencing notebook's assets folder
- Verify the notebook directory exists and resolves (ResolveRealPath succeeds) — recreate a damaged notebook folder from backup
- Move any referenced file that lives outside assets/ into the notebook's assets directory and update the link
- Audit data/ for symlinks leaving the workspace and remove them
Example fix
// before data/notebook1/assets/a.png -> ../notebook2/assets/a.png // after cp data/notebook2/assets/a.png data/notebook1/assets/a.png // real file, containment holds
Defensive patterns
Strategy: validation
Validate before calling
real, err := filepath.EvalSymlinks(absPath)
if err != nil { return err }
if !strings.HasPrefix(real, notebookAssetsRoot) {
return errors.New("asset resolves outside notebook assets")
} Try / catch
p, err := model.DeferredAssetPath(urlPath)
if err != nil && strings.Contains(err.Error(), "outside notebook assets") {
return fmt.Errorf("fix asset layout for %q: %w", urlPath, err)
} Prevention
- Do not symlink assets between notebooks; duplicate the file instead
- Keep notebook directories intact — don't move/rename folders behind the app's back
- Resolve symlinks (EvalSymlinks) when validating custom asset pipelines
- Audit unusual asset layouts after workspace restores
When it happens
Trigger: deferredAssetPath on an asset whose real path (after symlink resolution) lands outside the notebook's assets dir — e.g. an in-notebook symlink pointing to another notebook's assets, or the notebook root itself fails to resolve; exercised by TestDeferredAssetPathRejectsMissingLeafThroughEscapingSymlink.
Common situations: Assets symlinked across notebooks or from outside the data dir; a notebook directory was moved/renamed so stored roots no longer resolve; symlink-heavy setups after restoring a workspace from an archive.
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 [%s] resolves outside assets directory: [%s]
- symlink resolves into encrypted notebook [%s]
- archive entry resolves outside destination [%s]
- marketplace package contains an unsupported file
- symlink escapes workspace: %s
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/b359aac2fcc28f04.
Report an issue: GitHub.