siyuan-note/siyuan · error

symlink [%s] resolves outside assets directory: [%s]

Error message

symlink [%s] resolves outside assets directory: [%s]

What it means

Thrown by GetAssetAbsPathInBox (kernel/model/assets.go:1110) when a box-scoped asset is a symlink that resolves to a target outside the expected `<boxID>/assets/` prefix (even if it is still inside the workspace). It is a tighter guard than 411: the resolved real path must remain specifically under that box's assets directory (or the global `data/assets/`).

Source

Thrown at kernel/model/assets.go:1110

	}

	p := filepath.Join(util.DataDir, boxID, relativePath)
	if gulu.File.IsExist(p) {
		if !gulu.File.IsSubPath(util.WorkspaceDir, p) {
			return "", fmt.Errorf("[%s] is not sub path of workspace", p)
		}
		// 解析符号链接/目录联接,防止软链接跳出资产根目录
		if realP, evalErr := filepath.EvalSymlinks(p); evalErr == nil && realP != p {
			if !gulu.File.IsSubPath(util.WorkspaceDir, realP) {
				return "", fmt.Errorf("symlink [%s] resolves outside workspace: [%s]", p, realP)
			}
			// 验证解析后的路径仍在 <boxID>/assets/ 或全局 data/assets/ 下
			expectedPrefix := filepath.Join(util.DataDir, "assets")
			if boxID != "" {
				expectedPrefix = filepath.Join(util.DataDir, boxID, "assets")
			}
			if !gulu.File.IsSubPath(expectedPrefix, realP) {
				return "", fmt.Errorf("symlink [%s] resolves outside assets directory: [%s]", p, realP)
			}
		}
		return p, nil
	}
	// 非加密 box 的资源可能回退到全局 data/assets(兼容旧笔记本结构)
	if !IsEncryptedBox(boxID) {
		return GetAssetAbsPathWithOpt(relativePath, false)
	}
	return "", fmt.Errorf(Conf.Language(12), relativePath)
}

// GetAssetAbsPathWithOpt 与 GetAssetAbsPath 一致,但可通过 includeEncrypted 控制是否遍历加密 box。
// serveAssets 传 true(下游 serveEncryptedAsset 会按锁定状态 fail-closed),其他调用方传 false(安全跳过)。
func GetAssetAbsPathWithOpt(relativePath string, includeEncrypted bool) (string, error) {
	relativePath = strings.TrimSpace(relativePath)
	if idx := strings.Index(relativePath, "?"); idx >= 0 {
		relativePath = relativePath[:idx]
	}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Identify the link with `find <DataDir>/<boxID>/assets -type l -ls` and read its target.
  2. Replace it with a copy of the file that physically resides under `<boxID>/assets/`.
  3. If cross-box asset sharing is needed, copy the asset into each box's assets directory rather than linking.
  4. Treat unexpected occurrences as a security signal and audit the source document/request.
Defensive patterns

Strategy: validation

Validate before calling

// Ensure a box-asset symlink target stays within the box's assets dir.
full := filepath.Join(util.DataDir, boxID, filepath.FromSlash(rel))
expected := filepath.Join(util.DataDir, boxID, "assets")
if real, err := filepath.EvalSymlinks(full); err == nil && real != full {
    if !gulu.File.IsSubPath(expected, real) {
        return errors.New("box asset symlink escapes assets directory")
    }
}

Try / catch

if _, err := model.GetAssetAbsPathInBox(ref, box); err != nil && strings.Contains(err.Error(), "resolves outside assets directory") {
    // symlink escapes the box assets dir; replace with a copy, do not weaken the guard
}

Prevention

When it happens

Trigger: Calling GetAssetAbsPathInBox where the asset file is a symlink to another location inside the workspace but outside `<boxID>/assets/` — e.g. a link from `<boxA>/assets/x` to `<boxB>/data/notes/foo` or to `data/storage/...`. The workspace check (411) passes but this tighter check fails.

Common situations: Symlinking a box asset to a file in another box or to non-asset workspace data; cross-box sharing attempts via links; restoring a backup that recreated links with shifted targets.

Related errors


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