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
- Inspect the offending symlink with `ls -la <DataDir>/<boxID>/assets/` and identify links whose target is outside the workspace.
- Replace the escaping symlink with a real copy of the file placed inside the box's assets directory.
- If this is unexpected, treat it as a security event and audit where the link came from.
- 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
- Never symlink box assets to paths outside the workspace.
- Copy imported files physically into <boxID>/assets/.
- Audit box asset symlinks on shared/multi-user workspaces.
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
- notebook asset path resolves outside notebook directory: %s
- asset path resolves outside assets directory: %s
- symlink [%s] resolves outside assets directory: [%s]
- symlink [%s] resolves outside data/assets: [%s]
- asset path must be relative to data directory: %s
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/6bbea653715b8650.
Report an issue: GitHub.