{"record":{"id":"353fc09ca2dfcf24","repo":"siyuan-note/siyuan","slug":"s-is-not-sub-path-of-workspace","errorCode":null,"errorMessage":"[%s] is not sub path of workspace","messagePattern":"\\[(.+?)\\] is not sub path of workspace","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/assets.go","lineNumber":1240,"sourceCode":"\trelativePath = path.Clean(relativePath)\n\tif relativePath == \".\" || strings.HasPrefix(relativePath, \"../\") || relativePath == \"..\" || path.IsAbs(relativePath) {\n\t\treturn \"\", fmt.Errorf(\"[%s] is not an asset path\", relativePath)\n\t}\n\tif !strings.HasPrefix(relativePath, \"assets/\") {\n\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（兼容旧笔记本结构）","sourceCodeStart":1222,"sourceCodeEnd":1258,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/assets.go#L1222-L1258","documentation":"After joining <workspace>/data/<boxID>/<relativePath>, GetAssetAbsPathInBox confirms the existing path is a sub-path of the workspace directory before serving it. This error is thrown when the joined path itself lies outside the workspace — typically because DataDir or boxID were redirected, or because the joined string escaped via unusual components. It is a workspace containment guard applied before symlink evaluation.","triggerScenarios":"Calling GetAssetAbsPathInBox when util.WorkspaceDir/util.DataDir are overridden (e.g. in tests or portable mode) and the constructed path resolves outside the workspace, or when a mounted volume makes the box directory not a filesystem-descendant of WorkspaceDir.","commonSituations":"Custom test harnesses pointing data at /tmp while WorkspaceDir stays at another root; bind-mounting notebook directories into the workspace from elsewhere; misconfigured SIYUAN_WORKING_DIR or portable-data setups.","solutions":["Ensure the data directory physically lives under the workspace directory (DataDir must be a sub-path of WorkspaceDir)","Fix the workspace/data configuration (workspace switcher or CLI flags) so both point at the same tree","If using bind mounts or symlinks for notebooks, mount them inside <workspace>/data/ so containment checks pass"],"exampleFix":"// before: data dir outside workspace\nutil.WorkspaceDir = \"/home/u/ws\"; util.DataDir = \"/mnt/data\"\n// after: data inside workspace\nutil.DataDir = filepath.Join(util.WorkspaceDir, \"data\")","handlingStrategy":"validation","validationCode":"p := filepath.Join(util.DataDir, boxID, rel)\nif !gulu.File.IsSubPath(util.WorkspaceDir, p) {\n\treturn fmt.Errorf(\"path escapes workspace\")\n}","typeGuard":null,"tryCatchPattern":"abs, err := model.GetAssetAbsPathInBox(ref, boxID)\nif err != nil && strings.Contains(err.Error(), \"not sub path of workspace\") {\n\t// fix workspace/data configuration, then retry once\n}","preventionTips":["Keep util.DataDir strictly inside util.WorkspaceDir (default layout)","Do not bind-mount or symlink notebook directories into the workspace from outside the workspace tree","In tests, set WorkspaceDir and DataDir consistently before calling model functions"],"tags":["security","path-traversal","workspace"],"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"}