siyuan-note/siyuan · error

symlink [ ] resolves outside data/assets: [ ]

Error message

symlink [%s] resolves outside data/assets: [%s]

What it means

After resolving the asset symlink, getAssetAbsPath verifies the real target path is still a sub-path of the resolved data/assets root. The error "symlink [%s] resolves outside data/assets: [%s]" is returned when a symlinked asset points outside data/assets — a defense against symlink-based escapes from the assets sandbox.

Solutions

  1. Replace the symlink with a real copy of the target file inside data/assets
  2. If sharing assets across notebooks, move them into data/assets and reference via the standard assets/ path
  3. Recreate the symlink so its target lives inside data/assets (e.g. data/assets/shared/foo.png -> data/assets/foo.png)
  4. On containerized setups, mount the shared assets directory at data/assets instead of symlinking to it

Example fix

// before (filesystem)
// ln -s /home/user/photos/cat.png <workspace>/data/assets/cat.png
// after (filesystem)
// cp /home/user/photos/cat.png <workspace>/data/assets/cat.png
Defensive patterns

Strategy: validation

Validate before calling

real, _ := filepath.EvalSymlinks(candidate)
if !strings.HasPrefix(real, realAssetsRoot) {
    return errors.New("symlink target must stay inside data/assets")
}

Try / catch

if err != nil && strings.Contains(err.Error(), "resolves outside data/assets") {
    return fmt.Errorf("replace symlink %s with a real copy inside data/assets", relPath)
}

Prevention

When it happens

Trigger: getAssetAbsPath finds a file p that is a symlink whose EvalSymlinks target realP is not under the real data/assets directory — e.g. data/assets/foo.png -> /home/user/secret.png or -> a path in another notebook outside assets.

Common situations: Users manually symlinking shared assets from outside the workspace into data/assets; docker/container setups where assets are mounted elsewhere and linked; backup/restore tools recreating symlinks with wrong targets.

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/5bd6e6a88bc1e08e. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/assets.go:1303

}

func getAssetAbsPath(relativePath string, includeEncrypted bool) (absPath string, err error) {
	relativePath = filepath.ToSlash(relativePath)
	// 在 data 文件夹下搜索,主要是 data/assets 文件夹
	p := filepath.Join(util.DataDir, relativePath)
	if gulu.File.IsExist(p) {
		if !gulu.File.IsSubPath(util.WorkspaceDir, p) {
			return "", fmt.Errorf("[%s] is not sub path of workspace", p)
		}
		// 解析符号链接,验证真实路径仍在 data/assets/ 下
		if realP, evalErr := filepath.EvalSymlinks(p); evalErr == nil && realP != p {
			assetsRoot := util.GetDataAssetsAbsPath()
			realAssetsRoot, rootEvalErr := filepath.EvalSymlinks(assetsRoot)
			if rootEvalErr != nil {
				return "", fmt.Errorf("resolve assets root [%s] failed: %w", assetsRoot, rootEvalErr)
			}
			if !gulu.File.IsSubPath(realAssetsRoot, realP) {
				return "", fmt.Errorf("symlink [%s] resolves outside data/assets: [%s]", p, realP)
			}
			// 安全校验使用解析后的路径,返回原路径以便下游与 DataDir 保持同一路径形式
			return p, nil
		}
		return p, nil
	}

	// 在文档同级 assets 文件夹下搜索
	if !strings.HasPrefix(relativePath, "assets/") {
		return "", nil
	}
	notebooks, err := ListNotebooks()
	if err != nil {
		return "", errors.New(Conf.Language(0))
	}
	for _, notebook := range notebooks {
		if !includeEncrypted && IsEncryptedBox(notebook.ID) {
			continue // 加密笔记本的资源不参与全局路径解析(孤岛,资源不跨边界)

View on GitHub (pinned to 9f775e8a12)