siyuan-note/siyuan · error

symlink [%s] resolves outside workspace: [%s]

Error message

symlink [%s] resolves outside workspace: [%s]

What it means

Thrown by GetAssetAbsPathInBox (kernel/model/assets.go:1102) when a file under `<boxID>/assets/` is a symlink (or directory junction) whose resolved target leaves util.WorkspaceDir. It is the first of two symlink guards in the box-scoped resolver: it catches a link that escapes the entire workspace before the more specific assets-bound check (error 412).

Source

Thrown at kernel/model/assets.go:1102

		return "", fmt.Errorf("[%s] is not an asset path (must start with assets/)", relativePath)
	}
	if boxID != "" && !ast.IsNodeIDPattern(boxID) {
		return "", fmt.Errorf("[%s] is not a box id", boxID)
	}

	if boxID == "" {
		return GetAssetAbsPathWithOpt(relativePath, false)
	}

	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)
}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Inspect the offending symlink with `ls -la <DataDir>/<boxID>/assets/` and identify links whose target is outside the workspace.
  2. Replace the escaping symlink with a real copy of the file placed inside the box's assets directory.
  3. If this is unexpected, treat it as a security event and audit where the link came from.
  4. Do not weaken the check; it prevents reading arbitrary files outside the workspace.
Defensive patterns

Strategy: validation

Validate before calling

// Detect box-asset symlinks that escape the workspace before resolving.
full := filepath.Join(util.DataDir, boxID, filepath.FromSlash(rel))
if real, err := filepath.EvalSymlinks(full); err == nil && real != full {
    if !gulu.File.IsSubPath(util.WorkspaceDir, real) {
        return errors.New("box asset symlink escapes workspace")
    }
}

Try / catch

if _, err := model.GetAssetAbsPathInBox(ref, box); err != nil && strings.Contains(err.Error(), "resolves outside workspace") {
    // symlink escapes workspace; remove/replace the link, do not bypass
}

Prevention

When it happens

Trigger: Calling GetAssetAbsPathInBox for an asset whose file is a symlink pointing to a path outside WorkspaceDir (e.g. `<DataDir>/<box>/assets/secret` -> `/etc/passwd` or `../../external`). EvalSymlinks resolves the target, the workspace membership check fails, and the request is rejected.

Common situations: A user symlinked an asset to an external file to import it; a maliciously crafted document references such a link; a sync/restore created junctions pointing outside the workspace.

Related errors


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