siyuan-note/siyuan · error

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

Error message

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

What it means

Thrown by getAssetAbsPath (kernel/model/assets.go:1157) for global assets: after resolving symlinks on both the file and the `data/assets` root, the resolved file target is no longer inside the resolved `data/assets` root. This is the global-assets counterpart of error 404/412, catching symlinks inside `data/assets/` that escape it.

Source

Thrown at kernel/model/assets.go:1157

}

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 251596fc0d)

Solutions

  1. List symlinks under the assets root: `find <DataDir>/assets -type l -ls`.
  2. Replace escaping symlinks with real copies stored inside `data/assets/`.
  3. If importing external media, copy the files into `data/assets/` instead of symlinking across boundaries.
  4. Treat unexpected occurrences as a security event and audit the source of the reference.
Defensive patterns

Strategy: validation

Validate before calling

// Detect global-asset symlinks that escape data/assets before resolving.
full := filepath.Join(util.DataDir, filepath.FromSlash(rel))
root, _ := filepath.EvalSymlinks(util.GetDataAssetsAbsPath())
if real, err := filepath.EvalSymlinks(full); err == nil && real != full && root != "" {
    if !gulu.File.IsSubPath(root, real) {
        return errors.New("global asset symlink escapes data/assets")
    }
}

Try / catch

if _, err := model.GetAssetAbsPath(ref); err != nil && strings.Contains(err.Error(), "resolves outside data/assets") {
    // symlink under data/assets escapes it; replace with a copy, do not bypass
}

Prevention

When it happens

Trigger: Calling GetAssetAbsPath for a global asset like `assets/linked/x.png` where `data/assets/linked` is a symlink to a directory outside `data/assets/` (e.g. to `/home/user/pics`). The lexical path is fine, but EvalSymlinks reveals the target is outside assets.

Common situations: A user symlinked part of `data/assets/` to external storage to save space or import a library; a backup/restore created escaping junctions; a malicious document references a crafted path.

Related errors


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