{"record":{"id":"76c05a56402cac00","repo":"siyuan-note/siyuan","slug":"encrypted-box-asset-must-be-written-inside-the-box","errorCode":null,"errorMessage":"encrypted box asset must be written inside the box directory, got global path: %s","messagePattern":"encrypted box asset must be written inside the box directory, got global path: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/upload.go","lineNumber":567,"sourceCode":"\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：全读 → 加密 → 落盘\n\t\traw, readErr := io.ReadAll(src)\n\t\tif readErr != nil {\n\t\t\treturn readErr\n\t\t}","sourceCodeStart":549,"sourceCodeEnd":585,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/upload.go#L549-L585","documentation":"writeAssetFile refuses to write assets for an encrypted notebook to a path outside that notebook's directory (e.g. the global data/assets folder). Encrypted content must stay inside the box directory so it is encrypted at rest; writing to the global assets path would leak plaintext (or lose the encryption envelope).","triggerScenarios":"Calling writeAssetFile with boxID of an encrypted notebook and a writePath that ExtractBoxIDFromAssetsPath resolves to empty (a global path such as data/assets), via Upload, InsertAssetBytes, insertLocalAssets, netAssets2LocalAssets0, or importSYAssets.","commonSituations":"An API/plugin caller uploads an asset without specifying a target document, so getAssetsDir falls back to the global data/assets directory; automatic asset insertion into the default global assets folder for notebooks that are encrypted.","solutions":["Target the write path at the encrypted box's own assets directory (<boxLocalPath>/assets), creating it if missing","Check IsEncryptedBox(boxID) before choosing the global assets fallback in path-resolution code and skip that fallback for encrypted boxes","If the write is meant for global assets, explicitly pass an empty boxID only when the content is intended to be global (not from an encrypted notebook)"],"exampleFix":"// before\nassetsDir := filepath.Join(util.DataDir, \"assets\")\nerr := model.writeAssetFile(filepath.Join(assetsDir, name), src, boxID, name)\n// after\nassetsDir := filepath.Join(model.GetBoxLocalPath(boxID), \"assets\")\nos.MkdirAll(assetsDir, 0755)\nerr := model.writeAssetFile(filepath.Join(assetsDir, name), src, boxID, name)","handlingStrategy":"validation","validationCode":"func assertEncryptedBoxAssetPath(boxID, writePath string) error {\n    if boxID != \"\" && model.IsEncryptedBox(boxID) && model.ExtractBoxIDFromAssetsPath(writePath) == \"\" {\n        return fmt.Errorf(\"encrypted box %s asset must go inside the box dir, got %s\", boxID, writePath)\n    }\n    return nil\n}","typeGuard":"func isGlobalPathForEncryptedBox(boxID, writePath string) bool {\n    return boxID != \"\" && model.IsEncryptedBox(boxID) && model.ExtractBoxIDFromAssetsPath(writePath) == \"\"\n}","tryCatchPattern":"if err := model.writeAssetFile(writePath, src, boxID, name); err != nil {\n    if strings.HasPrefix(err.Error(), \"encrypted box asset must be written inside\") {\n        // relocate the write under the encrypted box's assets dir\n        writePath = filepath.Join(model.GetBoxLocalPath(boxID), \"assets\", name)\n        err = model.writeAssetFile(writePath, src, boxID, name)\n    }\n    return err\n}","preventionTips":["Never use the global data/assets fallback when the target notebook is encrypted — check IsEncryptedBox first in path resolution","Always resolve asset directories with getAssetsDir (which enforces box-local paths for encrypted boxes) rather than building paths manually","Require callers to pass the target document or box ID so encrypted-asset uploads cannot fall back to global paths"],"tags":["filesystem","encryption","upload","siyuan"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}