{"record":{"id":"6bbea653715b8650","repo":"siyuan-note/siyuan","slug":"symlink-s-resolves-outside-workspace-s","errorCode":null,"errorMessage":"symlink [%s] resolves outside workspace: [%s]","messagePattern":"symlink \\[(.+?)\\] resolves outside workspace: \\[(.+?)\\]","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":1102,"sourceCode":"\t\treturn \"\", fmt.Errorf(\"[%s] is not an asset path (must start with assets/)\", relativePath)\n\t}\n\tif boxID != \"\" && !ast.IsNodeIDPattern(boxID) {\n\t\treturn \"\", fmt.Errorf(\"[%s] is not a box id\", boxID)\n\t}\n\n\tif boxID == \"\" {\n\t\treturn GetAssetAbsPathWithOpt(relativePath, false)\n\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}","sourceCodeStart":1084,"sourceCodeEnd":1120,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/assets.go#L1084-L1120","documentation":"Thrown by GetAssetAbsPathInBox (kernel/model/assets.go:1102) when a file under `<boxID>/assets/` is a symlink (or directory junction) whose resolved target leaves util.WorkspaceDir. It is the first of two symlink guards in the box-scoped resolver: it catches a link that escapes the entire workspace before the more specific assets-bound check (error 412).","triggerScenarios":"Calling GetAssetAbsPathInBox for an asset whose file is a symlink pointing to a path outside WorkspaceDir (e.g. `<DataDir>/<box>/assets/secret` -> `/etc/passwd` or `../../external`). EvalSymlinks resolves the target, the workspace membership check fails, and the request is rejected.","commonSituations":"A user symlinked an asset to an external file to import it; a maliciously crafted document references such a link; a sync/restore created junctions pointing outside the workspace.","solutions":["Inspect the offending symlink with `ls -la <DataDir>/<boxID>/assets/` and identify links whose target is outside the workspace.","Replace the escaping symlink with a real copy of the file placed inside the box's assets directory.","If this is unexpected, treat it as a security event and audit where the link came from.","Do not weaken the check; it prevents reading arbitrary files outside the workspace."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Detect box-asset symlinks that escape the workspace before resolving.\nfull := filepath.Join(util.DataDir, boxID, filepath.FromSlash(rel))\nif real, err := filepath.EvalSymlinks(full); err == nil && real != full {\n    if !gulu.File.IsSubPath(util.WorkspaceDir, real) {\n        return errors.New(\"box asset symlink escapes workspace\")\n    }\n}","typeGuard":null,"tryCatchPattern":"if _, err := model.GetAssetAbsPathInBox(ref, box); err != nil && strings.Contains(err.Error(), \"resolves outside workspace\") {\n    // symlink escapes workspace; remove/replace the link, do not bypass\n}","preventionTips":["Never symlink box assets to paths outside the workspace.","Copy imported files physically into <boxID>/assets/.","Audit box asset symlinks on shared/multi-user workspaces."],"tags":["assets","symlinks","path-traversal","security","workspace"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}