{"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":"error","filePath":"kernel/model/assets.go","lineNumber":1110,"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 !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)\n\tif idx := strings.Index(relativePath, \"?\"); idx >= 0 {\n\t\trelativePath = relativePath[:idx]\n\t}","sourceCodeStart":1092,"sourceCodeEnd":1128,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/assets.go#L1092-L1128","documentation":"Thrown by GetAssetAbsPathInBox (kernel/model/assets.go:1110) when a box-scoped asset is a symlink that resolves to a target outside the expected `<boxID>/assets/` prefix (even if it is still inside the workspace). It is a tighter guard than 411: the resolved real path must remain specifically under that box's assets directory (or the global `data/assets/`).","triggerScenarios":"Calling GetAssetAbsPathInBox where the asset file is a symlink to another location inside the workspace but outside `<boxID>/assets/` — e.g. a link from `<boxA>/assets/x` to `<boxB>/data/notes/foo` or to `data/storage/...`. The workspace check (411) passes but this tighter check fails.","commonSituations":"Symlinking a box asset to a file in another box or to non-asset workspace data; cross-box sharing attempts via links; restoring a backup that recreated links with shifted targets.","solutions":["Identify the link with `find <DataDir>/<boxID>/assets -type l -ls` and read its target.","Replace it with a copy of the file that physically resides under `<boxID>/assets/`.","If cross-box asset sharing is needed, copy the asset into each box's assets directory rather than linking.","Treat unexpected occurrences as a security signal and audit the source document/request."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Ensure a box-asset symlink target stays within the box's assets dir.\nfull := filepath.Join(util.DataDir, boxID, filepath.FromSlash(rel))\nexpected := filepath.Join(util.DataDir, boxID, \"assets\")\nif real, err := filepath.EvalSymlinks(full); err == nil && real != full {\n    if !gulu.File.IsSubPath(expected, real) {\n        return errors.New(\"box asset symlink escapes assets directory\")\n    }\n}","typeGuard":null,"tryCatchPattern":"if _, err := model.GetAssetAbsPathInBox(ref, box); err != nil && strings.Contains(err.Error(), \"resolves outside assets directory\") {\n    // symlink escapes the box assets dir; replace with a copy, do not weaken the guard\n}","preventionTips":["Do not link box assets to other boxes or to non-asset workspace data.","Copy shared assets into each box that needs them.","Audit `find <DataDir>/<boxID>/assets -type l` regularly."],"tags":["assets","symlinks","path-traversal","security","notebooks"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}