{"record":{"id":"5bd6e6a88bc1e08e","repo":"siyuan-note/siyuan","slug":"symlink-s-resolves-outside-data-assets-s","errorCode":null,"errorMessage":"symlink [%s] resolves outside data/assets: [%s]","messagePattern":"symlink \\[(.+?)\\] resolves outside data/assets: \\[(.+?)\\]","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":1157,"sourceCode":"}\n\nfunc getAssetAbsPath(relativePath string, includeEncrypted bool) (absPath string, err error) {\n\trelativePath = filepath.ToSlash(relativePath)\n\t// 在 data 文件夹下搜索，主要是 data/assets 文件夹\n\tp := filepath.Join(util.DataDir, 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// 解析符号链接，验证真实路径仍在 data/assets/ 下\n\t\tif realP, evalErr := filepath.EvalSymlinks(p); evalErr == nil && realP != p {\n\t\t\tassetsRoot := util.GetDataAssetsAbsPath()\n\t\t\trealAssetsRoot, rootEvalErr := filepath.EvalSymlinks(assetsRoot)\n\t\t\tif rootEvalErr != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"resolve assets root [%s] failed: %w\", assetsRoot, rootEvalErr)\n\t\t\t}\n\t\t\tif !gulu.File.IsSubPath(realAssetsRoot, realP) {\n\t\t\t\treturn \"\", fmt.Errorf(\"symlink [%s] resolves outside data/assets: [%s]\", p, realP)\n\t\t\t}\n\t\t\t// 安全校验使用解析后的路径，返回原路径以便下游与 DataDir 保持同一路径形式\n\t\t\treturn p, nil\n\t\t}\n\t\treturn p, nil\n\t}\n\n\t// 在文档同级 assets 文件夹下搜索\n\tif !strings.HasPrefix(relativePath, \"assets/\") {\n\t\treturn \"\", nil\n\t}\n\tnotebooks, err := ListNotebooks()\n\tif err != nil {\n\t\treturn \"\", errors.New(Conf.Language(0))\n\t}\n\tfor _, notebook := range notebooks {\n\t\tif !includeEncrypted && IsEncryptedBox(notebook.ID) {\n\t\t\tcontinue // 加密笔记本的资源不参与全局路径解析（孤岛，资源不跨边界）","sourceCodeStart":1139,"sourceCodeEnd":1175,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/assets.go#L1139-L1175","documentation":"Thrown by getAssetAbsPath (kernel/model/assets.go:1157) for global assets: after resolving symlinks on both the file and the `data/assets` root, the resolved file target is no longer inside the resolved `data/assets` root. This is the global-assets counterpart of error 404/412, catching symlinks inside `data/assets/` that escape it.","triggerScenarios":"Calling GetAssetAbsPath for a global asset like `assets/linked/x.png` where `data/assets/linked` is a symlink to a directory outside `data/assets/` (e.g. to `/home/user/pics`). The lexical path is fine, but EvalSymlinks reveals the target is outside assets.","commonSituations":"A user symlinked part of `data/assets/` to external storage to save space or import a library; a backup/restore created escaping junctions; a malicious document references a crafted path.","solutions":["List symlinks under the assets root: `find <DataDir>/assets -type l -ls`.","Replace escaping symlinks with real copies stored inside `data/assets/`.","If importing external media, copy the files into `data/assets/` instead of symlinking across boundaries.","Treat unexpected occurrences as a security event and audit the source of the reference."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Detect global-asset symlinks that escape data/assets before resolving.\nfull := filepath.Join(util.DataDir, filepath.FromSlash(rel))\nroot, _ := filepath.EvalSymlinks(util.GetDataAssetsAbsPath())\nif real, err := filepath.EvalSymlinks(full); err == nil && real != full && root != \"\" {\n    if !gulu.File.IsSubPath(root, real) {\n        return errors.New(\"global asset symlink escapes data/assets\")\n    }\n}","typeGuard":null,"tryCatchPattern":"if _, err := model.GetAssetAbsPath(ref); err != nil && strings.Contains(err.Error(), \"resolves outside data/assets\") {\n    // symlink under data/assets escapes it; replace with a copy, do not bypass\n}","preventionTips":["Do not symlink inside data/assets to external locations.","Copy imported media into data/assets physically.","Audit `find <DataDir>/assets -type l` periodically."],"tags":["assets","symlinks","path-traversal","security","filesystem"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}