{"record":{"id":"7d44f86a64a71b97","repo":"siyuan-note/siyuan","slug":"symlink-s-resolves-outside-assets-directory","errorCode":null,"errorMessage":"symlink [%s] resolves outside assets directory: [%s]","messagePattern":"symlink \\[(.+?)\\] resolves outside assets directory: \\[(.+?)\\]","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/assets.go","lineNumber":1253,"sourceCode":"\t}\n\n\tp := filepath.Join(util.DataDir, boxID, relativePath)\n\tif gulu.File.IsExist(p) {\n\t\tif !gulu.File.IsSubPath(util.WorkspaceDir, p) {\n\t\t\treturn \"\", fmt.Errorf(\"[%s] is not sub path of workspace\", p)\n\t\t}\n\t\t// 解析符号链接/目录联接，防止软链接跳出资产根目录\n\t\tif realP, evalErr := filepath.EvalSymlinks(p); evalErr == nil && realP != p {\n\t\t\tif !gulu.File.IsSubPath(util.WorkspaceDir, realP) {\n\t\t\t\treturn \"\", fmt.Errorf(\"symlink [%s] resolves outside workspace: [%s]\", p, realP)\n\t\t\t}\n\t\t\t// 验证解析后的路径仍在 <boxID>/assets/ 或全局 data/assets/ 下\n\t\t\texpectedPrefix := filepath.Join(util.DataDir, \"assets\")\n\t\t\tif boxID != \"\" {\n\t\t\t\texpectedPrefix = filepath.Join(util.DataDir, boxID, \"assets\")\n\t\t\t}\n\t\t\tif !gulu.File.IsSubPath(expectedPrefix, realP) {\n\t\t\t\treturn \"\", fmt.Errorf(\"symlink [%s] resolves outside assets directory: [%s]\", p, realP)\n\t\t\t}\n\t\t}\n\t\treturn p, nil\n\t}\n\t// 非加密 box 的资源可能回退到全局 data/assets（兼容旧笔记本结构）\n\tif deferredPath, deferredErr := deferredAssetPath(relativePath, boxID, true); deferredErr != nil || deferredPath != \"\" {\n\t\treturn deferredPath, deferredErr\n\t}\n\tif !IsEncryptedBox(boxID) {\n\t\treturn GetAssetAbsPathWithOpt(relativePath, false)\n\t}\n\treturn \"\", fmt.Errorf(Conf.Language(12), relativePath)\n}\n\n// GetAssetAbsPathWithOpt 与 GetAssetAbsPath 一致，但可通过 includeEncrypted 控制是否遍历加密 box。\n// serveAssets 传 true（下游 serveEncryptedAsset 会按锁定状态 fail-closed），其他调用方传 false（安全跳过）。\nfunc GetAssetAbsPathWithOpt(relativePath string, includeEncrypted bool) (string, error) {\n\trelativePath = strings.TrimSpace(relativePath)","sourceCodeStart":1235,"sourceCodeEnd":1271,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/assets.go#L1235-L1271","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: cross-notebook symlink\nln -s ../../otherBox/assets/img.png data/box/assets/img.png\n// after: real copy in own assets\ncp data/otherBox/assets/img.png data/box/assets/img.png","handlingStrategy":"validation","validationCode":"real, err := filepath.EvalSymlinks(p)\nif err == nil {\n\tprefix := filepath.Join(util.DataDir, boxID, \"assets\")\n\tif !gulu.File.IsSubPath(prefix, real) {\n\t\treturn fmt.Errorf(\"symlink escapes the notebook assets dir\")\n\t}\n}","typeGuard":null,"tryCatchPattern":"abs, err := model.GetAssetAbsPathInBox(ref, boxID)\nif err != nil && strings.Contains(err.Error(), \"resolves outside assets directory\") {\n\t// copy the cross-notebook target into this box's assets/ and retry\n}","preventionTips":["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"],"tags":["security","symlink","asset-resolution"],"backgroundTag":"path-traversal-blocked","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}