siyuan-note/siyuan · warning
symlink [ ] resolves outside assets directory: [ ]
Error message
symlink [%s] resolves outside assets directory: [%s]
What it means
After confirming the real path is within the workspace, GetAssetAbsPathInBox further requires that the symlink-resolved path still lands inside <boxID>/assets/ (or data/assets/ for deferred/global fallback). This error is returned when a link stays inside the workspace but points from one notebook's assets into a different location, e.g. another notebook's assets or an unrelated data folder.
Solutions
- Copy the target file into the current notebook's data/<boxID>/assets/ and replace the symlink with a real file
- Reference the asset via the other notebook's boxID (or the global assets/ folder) instead of a cross-notebook link
- Remove the symlink if the target no longer matters and re-insert the asset through the editor so it lands in the right assets directory
Example fix
// before: cross-notebook symlink ln -s ../../otherBox/assets/img.png data/box/assets/img.png // after: real copy in own assets cp data/otherBox/assets/img.png data/box/assets/img.png
Defensive patterns
Strategy: validation
Validate before calling
real, err := filepath.EvalSymlinks(p)
if err == nil {
prefix := filepath.Join(util.DataDir, boxID, "assets")
if !gulu.File.IsSubPath(prefix, real) {
return fmt.Errorf("symlink escapes the notebook assets dir")
}
} Try / catch
abs, err := model.GetAssetAbsPathInBox(ref, boxID)
if err != nil && strings.Contains(err.Error(), "resolves outside assets directory") {
// copy the cross-notebook target into this box's assets/ and retry
} Prevention
- Do not symlink assets across notebooks; copy files instead
- Avoid dedup scripts that replace asset files with links to shared locations
- When an asset belongs to another notebook, reference it with that notebook's boxID rather than linking
When it happens
Trigger: A symlink in data/<boxID>/assets/foo.png points to data/otherBox/assets/foo.png or to data/someOtherDir/file — inside the workspace but outside the expected assets prefix; calling GetAssetAbsPathInBox with the box whose assets contain such a cross-link.
Common situations: Users linking assets between notebooks with relative symlinks; deduplication scripts that replaced duplicate asset files with hardlinks/symlinks to a shared folder; a notebook renamed/merged leaving stale cross-notebook links.
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
- asset path resolves outside assets directory
- archive entry resolves outside destination
- asset path contains an unresolved symbolic link
- asset path resolves outside notebook assets directory
- child template path is outside the current template package
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/7d44f86a64a71b97.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/assets.go:1253
}
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)
}
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)View on GitHub (pinned to 9f775e8a12)