{"record":{"id":"bd78e10aae4ccca3","repo":"siyuan-note/siyuan","slug":"boxid-mismatch-param-s-path-s","errorCode":null,"errorMessage":"boxID mismatch: param=%s, path=%s","messagePattern":"boxID mismatch: param=(.+?), path=(.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/upload.go","lineNumber":563,"sourceCode":"\t\t\t\t_ = os.MkdirAll(assets, 0755)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassets = filepath.Join(util.DataDir, \"assets\")\n\t\t}\n\t}\n\treturn\n}\n\n// writeAssetFile 把 src 的内容写入 writePath。从 writePath 反查真实 boxID 决定是否加密——\n// 不轻信传入的 boxID（调用方可能未传，或 assetsDirPath 指向加密笔记本但 id 为空）。\n// 加密笔记本必须已解锁（DEK 在内存）才写入；加密但未解锁返回错误（fail-closed，避免明文落盘）。\n// 非加密笔记本按 reader 直接写（走 filelock.WriteFileByReader 原路径，保留锁语义）。\nfunc writeAssetFile(writePath string, src io.Reader, boxID, originalName string) (err error) {\n\t// 从 writePath 反查真实 boxID，与传入 boxID 交叉校验\n\tpathBoxID := ExtractBoxIDFromAssetsPath(writePath)\n\t// 传入 boxID 与路径 box 都非空但不一致：路径指向另一个 box，拒绝（防跨 box 写入）\n\tif boxID != \"\" && pathBoxID != \"\" && boxID != pathBoxID {\n\t\treturn fmt.Errorf(\"boxID mismatch: param=%s, path=%s\", boxID, pathBoxID)\n\t}\n\t// 路径不在 box 下但传入的是加密 box：加密内容只能写 box 内，拒绝写全局 assets\n\tif pathBoxID == \"\" && boxID != \"\" && IsEncryptedBox(boxID) {\n\t\treturn fmt.Errorf(\"encrypted box asset must be written inside the box directory, got global path: %s\", writePath)\n\t}\n\tactualBoxID := pathBoxID\n\tif actualBoxID == \"\" {\n\t\tactualBoxID = boxID // 路径不在 box 下（如全局 assets），回退传入值\n\t}\n\tif actualBoxID != \"\" && IsEncryptedBox(actualBoxID) {\n\t\tHoldBoxReadLock(actualBoxID)\n\t\tdefer ReleaseBoxReadLock(actualBoxID)\n\t\tdek, dekErr := GetDEKIfUnlocked(actualBoxID)\n\t\tif dekErr != nil {\n\t\t\t// 加密笔记本未解锁：拒绝写入，避免明文落盘（深度防御，见 issue #18034）\n\t\t\treturn dekErr\n\t\t}\n\t\t// 已解锁的加密 box：全读 → 加密 → 落盘","sourceCodeStart":545,"sourceCodeEnd":581,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/upload.go#L545-L581","documentation":"writeAssetFile cross-validates the caller-supplied boxID against the box ID reverse-extracted from the target asset write path. If both are non-empty and point to different notebooks, the write is rejected to prevent cross-box writes (an asset intended for one box silently landing in another notebook's assets directory).","triggerScenarios":"Calling writeAssetFile (directly or via Upload, InsertAssetBytes, insertLocalAssets, netAssets2LocalAssets0, importSYAssets) with a writePath under one notebook's assets directory while passing a different, non-empty boxID parameter.","commonSituations":"Plugin or API callers that cache a notebook ID from a previous operation but resolve the asset path from the currently-focused document in a different notebook; batch upload loops that reuse a stale boxID; code that hardcodes a default boxID while the assets path resolves to data/assets or another box.","solutions":["Check which notebook the writePath actually belongs to (ExtractBoxIDFromAssetsPath) and pass that box ID as the boxID parameter, or pass \"\" to defer entirely to the path","If the asset really belongs to the boxID given, rebuild writePath under that box's assets directory before calling","If paths and box IDs are derived dynamically, log both values and fix the upstream derivation so they stay consistent"],"exampleFix":"// before\nerr := model.writeAssetFile(filepath.Join(otherBoxAssetsPath, name), src, boxID, name)\n// after\npathBoxID := model.ExtractBoxIDFromAssetsPath(filepath.Join(otherBoxAssetsPath, name))\nerr := model.writeAssetFile(filepath.Join(otherBoxAssetsPath, name), src, pathBoxID, name)","handlingStrategy":"validation","validationCode":"func canWriteAsset(writePath, boxID string) error {\n    pathBoxID := model.ExtractBoxIDFromAssetsPath(writePath)\n    if boxID != \"\" && pathBoxID != \"\" && boxID != pathBoxID {\n        return fmt.Errorf(\"asset path %s belongs to box %q, not %q\", writePath, pathBoxID, boxID)\n    }\n    return nil\n}","typeGuard":"func boxMatchesPath(boxID, writePath string) bool {\n    pathBoxID := model.ExtractBoxIDFromAssetsPath(writePath)\n    return boxID == \"\" || pathBoxID == \"\" || boxID == pathBoxID\n}","tryCatchPattern":"if err := model.writeAssetFile(writePath, src, boxID, name); err != nil {\n    if strings.HasPrefix(err.Error(), \"boxID mismatch\") {\n        // recompute the correct box ID from writePath and retry once\n        pathBoxID := model.ExtractBoxIDFromAssetsPath(writePath)\n        err = model.writeAssetFile(writePath, src, pathBoxID, name)\n    }\n    return err\n}","preventionTips":["Always derive the box ID from the resolved write path instead of passing a caller-cached value","Pass \"\" for boxID when the target path is authoritative and you do not need cross-validation","Add a unit test asserting the box ID and asset path come from the same notebook for every upload entry point"],"tags":["filesystem","upload","validation","siyuan"],"backgroundTag":"invalid-argument-value","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}