siyuan-note/siyuan · warning

asset path resolves outside assets directory

Error message

asset path resolves outside assets directory: %s

What it means

ResolveDataAssetPath resolves the asset path (following symlinks) and then verifies that the resolved real path still lives under the notebook/global assets root. This error is thrown when the final resolved location escapes the assets directory — the path only appeared to be inside assets/ before symlink resolution. It is a path-traversal containment check.

Solutions

  1. Inspect the symlink chain: `readlink -f <workspace>/data/<path>` to see where it actually points
  2. Move the real file into <workspace>/data/.../assets/ and replace the symlink with a regular file
  3. If sharing assets across notebooks, copy the file rather than linking it, or reference the global data/assets/ location
  4. Verify the link target is not on a bind-mount or network share whose path lies outside the workspace

Example fix

// before: symlink escapes assets
ln -s /var/data/img.png data/20250101120000-abc/assets/img.png
// after: real file inside assets
cp /var/data/img.png data/20250101120000-abc/assets/img.png
Defensive patterns

Strategy: validation

Validate before calling

real, err := filepath.EvalSymlinks(absPath)
if err == nil && !strings.HasPrefix(real, filepath.EvalSymlinks(assetRoot)) {
	// symlink escapes assets; do not call the resolver
}

Try / catch

rel, abs, err := model.ResolveDataAssetPath(p)
if err != nil && strings.Contains(err.Error(), "resolves outside assets directory") {
	// copy the real file into assets/ and replace the symlink, then retry
}

Prevention

When it happens

Trigger: Calling ResolveDataAssetPath with a path inside <box>/assets/ that is a symlink (or contains a symlinked parent directory) resolving to a location outside the resolved assets root, e.g. symlink to /etc/passwd or to another notebook's assets when the notebook root check passed but the asset root containment failed.

Common situations: Manually symlinking assets from outside the workspace into a notebook for convenience; restoring a workspace from a backup that captured symlinks; a malicious or corrupted document referencing a crafted asset path.

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@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/58ebd3b080652937. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/assets.go:1103

	}
	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 相对资源路径,并确认目标当前未被引用。
func ResolveUnusedDataAssetPath(assetPath string) (relativePath, absPath string, err error) {
	relativePath, absPath, err = ResolveDataAssetPath(assetPath)
	if err != nil {
		return
	}

	if unusedAssetsContainPath(relativePath, absPath, UnusedAssets(false)) {
		return
	}
	err = fmt.Errorf("asset is not unused: %s", relativePath)

View on GitHub (pinned to 9f775e8a12)