siyuan-note/siyuan · warning
symlink [ ] resolves outside workspace: [ ]
Error message
symlink [%s] resolves outside workspace: [%s]
What it means
When the constructed asset path exists, GetAssetAbsPathInBox evaluates symlinks/junctions with filepath.EvalSymlinks and requires the real path to remain inside the workspace. This error is returned when a symlink inside <boxID>/assets/ resolves to a location outside the workspace — a containment check that defeats links escaping the data tree.
Solutions
- Copy the target file into data/<boxID>/assets/ and replace the symlink with a real file
- If sharing content, move the referenced files under the workspace and update documents' links
- Remove the dangling or external symlink; resync the notebook so real files are present
Example fix
// before: link out of workspace ln -s ~/Pictures/img.png data/box/assets/img.png // after: file inside workspace cp ~/Pictures/img.png data/box/assets/img.png && rm data/box/assets/img.png.old
Defensive patterns
Strategy: validation
Validate before calling
if real, err := filepath.EvalSymlinks(p); err == nil && !gulu.File.IsSubPath(util.WorkspaceDir, real) {
return fmt.Errorf("symlink target outside workspace")
} Try / catch
abs, err := model.GetAssetAbsPathInBox(ref, boxID)
if err != nil && strings.Contains(err.Error(), "resolves outside workspace") {
// copy the target into data/<boxID>/assets/ and replace the symlink, then retry
} Prevention
- Store assets as regular files inside the workspace; avoid out-of-workspace symlinks
- After syncing/restoring workspaces, scan for symlinks with external targets
- Document to users that externally linked assets must be copied into assets/
When it happens
Trigger: An asset in data/<boxID>/assets/ is a symlink (or sits under a symlinked directory) pointing outside the workspace, e.g. to the user's home directory, an external drive, or another application's data; calling GetAssetAbsPathInBox with a boxID resolves that file and hits the check.
Common situations: Convenience symlinks to assets stored outside SiYuan; restoring a workspace archive that recorded absolute symlink targets from another machine; cloud-sync clients materializing links whose targets were never synced.
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
- archive entry resolves outside destination
- asset path resolves outside assets directory
- child template path is outside the current template package
- notebook asset path resolves outside notebook directory
- symlink [ ] resolves outside data/assets: [ ]
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/6bbea653715b8650.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/assets.go:1245
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 deferredPath, deferredErr := deferredAssetPath(relativePath, boxID, true); deferredErr != nil || deferredPath != "" {
return deferredPath, deferredErr
}
if !IsEncryptedBox(boxID) {
return GetAssetAbsPathWithOpt(relativePath, false)View on GitHub (pinned to 9f775e8a12)