{"record":{"id":"6544da5e98e52067","repo":"siyuan-note/siyuan","slug":"s-is-not-a-box-id","errorCode":null,"errorMessage":"[%s] is not a box id","messagePattern":"\\[(.+?)\\] is not a box id","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":1087,"sourceCode":"\n// GetAssetAbsPathInBox 在指定 box 内解析资源绝对路径，不进行全局遍历。\n// relativePath 必须以 assets/ 前缀开头，boxID 为空且路径没有 box 查询参数时只解析普通/全局资源，不遍历加密 box。\n// 加密 box 直接从 <boxID>/assets/ 查找，不依赖后缀匹配。\nfunc GetAssetAbsPathInBox(relativePath, boxID string) (string, error) {\n\tvar err error\n\trelativePath, boxID, err = assetPathAndBox(relativePath, boxID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\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\")","sourceCodeStart":1069,"sourceCodeEnd":1105,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/assets.go#L1069-L1105","documentation":"Thrown by GetAssetAbsPathInBox (kernel/model/assets.go:1087) when a non-empty boxID does not match ast.IsNodeIDPattern — SiYuan's notebook ID format (a timestamp-style identifier like `20250812143022-abcdefg`). It rejects malformed box IDs (whether supplied by the caller or parsed from a `?box=` query param) before they are joined into a filesystem path.","triggerScenarios":"Calling GetAssetAbsPathInBox(path, boxID) where boxID is something like `../foo`, `assets`, an empty-ish string of spaces, or any value that does not fit the node-ID grammar. Also reached when a `?box=` query param in relativePath holds a garbage value (because assetPathAndBox promotes it into boxID).","commonSituations":"Frontend/HTTP caller passing the notebook name instead of its ID; a crafted `?box=` query param used as a traversal vector (e.g. `?box=..`); stale code that passes a human-readable slug.","solutions":["Always pass the canonical notebook ID (obtain via model.ListNotebooks or the box's conf.json), never the notebook name.","If the box came from a URL query param, validate it with ast.IsNodeIDPattern before calling.","If no specific box is intended, pass an empty string rather than a placeholder.","Treat malformed box IDs from HTTP input as a likely attack and return 400."],"exampleFix":"// before\nabs, err := model.GetAssetAbsPathInBox(rel, boxFromUser)\n\n// after\nif boxFromUser != \"\" && !ast.IsNodeIDPattern(boxFromUser) {\n    return \"\", fmt.Errorf(\"invalid box id: %q\", boxFromUser)\n}\nabs, err := model.GetAssetAbsPathInBox(rel, boxFromUser)","handlingStrategy":"validation","validationCode":"import \"github.com/88250/lute/ast\"\n\nif boxID != \"\" && !ast.IsNodeIDPattern(boxID) {\n    return fmt.Errorf(\"invalid box id: %q\", boxID)\n}","typeGuard":"func isValidBoxID(boxID string) bool {\n    return boxID == \"\" || ast.IsNodeIDPattern(boxID)\n}","tryCatchPattern":null,"preventionTips":["Always use the notebook's canonical ID (from ListNotebooks), not its name or slug.","Validate box IDs parsed from URL query params with ast.IsNodeIDPattern.","Pass an empty string rather than a placeholder when no box is intended."],"tags":["assets","notebooks","validation","box-id","security"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}