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

  1. Replace cross-notebook symlinks with real files inside the referencing notebook's assets folder
  2. Verify the notebook directory exists and resolves (ResolveRealPath succeeds) — recreate a damaged notebook folder from backup
  3. Move any referenced file that lives outside assets/ into the notebook's assets directory and update the link
  4. 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

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


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/b359aac2fcc28f04. Report an issue: GitHub.