siyuan-note/siyuan · error

asset path resolves outside assets directory: %s

Error message

asset path resolves outside assets directory: %s

What it means

Thrown by ResolveDataAssetPath (kernel/model/assets.go:971) after successfully resolving symlinks on both the assets root and the asset file: if the resolved file path no longer lives under the resolved assets root, the request is rejected. This catches symlinks inside the assets directory that point outside it, and is the core anti-traversal guard for global assets, explicitly tested by TestResolveDataAssetPath (`assets/linked/outside.png`).

Source

Thrown at kernel/model/assets.go:971

	}
	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 相对资源路径,并确认目标当前未被引用。
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 251596fc0d)

Solutions

  1. Inspect symlinks under the assets root: `find <DataDir>/assets -type l -ls`.
  2. Replace any escaping symlink with a copy of the file inside `data/assets/`, or remove the symlink.
  3. If importing external assets, copy them into `data/assets/` rather than symlinking across directory boundaries.
  4. Treat unexpected triggers as a security event and audit the originating document's asset references.
Defensive patterns

Strategy: validation

Validate before calling

// Ensure a global asset symlink target stays inside the assets root before resolving.
full := filepath.Join(util.DataDir, filepath.FromSlash(p))
real, err1 := filepath.EvalSymlinks(full)
root, err2 := filepath.EvalSymlinks(filepath.Join(util.DataDir, "assets"))
if err1 == nil && err2 == nil && !gulu.File.IsSubPath(root, real) {
    return errors.New("asset symlink escapes assets directory")
}

Try / catch

if _, _, err := model.ResolveDataAssetPath(p); err != nil && strings.Contains(err.Error(), "resolves outside assets directory") {
    // a symlink under data/assets points outside; audit and remove the link, do not bypass
}

Prevention

When it happens

Trigger: Calling ResolveDataAssetPath with a path like `assets/linked/outside.png` where `data/assets/linked` is a symlink to a directory outside `data/assets/`. The lexical path passes earlier checks, but after EvalSymlinks the real target is outside assets, so this fires.

Common situations: A user symlinked `data/assets/old` to an external folder to save space or import old data; a malicious document references a crafted asset path; a backup tool created junction points/symlinks that leave the assets tree.

Related errors


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