{"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":1230,"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":1212,"sourceCodeEnd":1248,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/assets.go#L1212-L1248","documentation":"When a boxID was supplied (from the argument or the ?box= query), GetAssetAbsPathInBox validates it against ast.IsNodeIDPattern, the canonical notebook-ID format (20-char timestamp-random pattern). This error is returned when the box ID does not match that pattern, preventing lookups into a non-existent or malformed notebook directory that could double as a traversal vector.","triggerScenarios":"Calling GetAssetAbsPathInBox(\"assets/img.png\", boxID) where boxID is a notebook name, a title, an empty-but-non-nil string with whitespace, a truncated ID, or a value like \"../x\" embedded in the ?box= query parameter.","commonSituations":"Passing a human-readable notebook name instead of its ID; building the box ID from user input or a plugin config value; a corrupted stored link whose ?box= query was mangled; extracting the wrong capture group from a URL regex.","solutions":["Pass the notebook's real 20-character ID (available from Conf.Box names/IDs or listNotebooks API), not its name","Trim whitespace and verify the ID against ast.IsNodeIDPattern before calling","Strip or fix the ?box= query parameter if the malformed ID comes from an asset URL"],"exampleFix":"// before: name instead of ID\nmodel.GetAssetAbsPathInBox(\"assets/img.png\", \"My Notebook\")\n// after: resolve the real box ID first\nboxID := getBoxIDByName(\"My Notebook\")\nmodel.GetAssetAbsPathInBox(\"assets/img.png\", boxID)","handlingStrategy":"validation","validationCode":"import \"github.com/88250/lute/ast\"\nif boxID != \"\" && !ast.IsNodeIDPattern(boxID) {\n\treturn fmt.Errorf(\"invalid box id %q\", boxID)\n}","typeGuard":"func validBoxID(id string) bool { return id == \"\" || ast.IsNodeIDPattern(id) }","tryCatchPattern":"abs, err := model.GetAssetAbsPathInBox(ref, boxID)\nif err != nil && strings.Contains(err.Error(), \"is not a box id\") {\n\t// resolve the real box ID from the notebook list and retry\n\tfor _, b := range model.Conf.Boxes { if b.Name == wantedName { abs, err = model.GetAssetAbsPathInBox(ref, b.ID) } }\n}","preventionTips":["Always obtain box IDs from the notebook list API, never from names or user free-text","Trim and validate any ?box= query values parsed from stored asset URLs","Sanitize plugin/config inputs that feed boxID parameters"],"tags":["validation","notebook","identifier"],"backgroundTag":"invalid-identifier-format","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"}