{"record":{"id":"ac4c8821820b1b64","repo":"siyuan-note/siyuan","slug":"refuse-to-write-decrypted-asset-inside-workspace","errorCode":null,"errorMessage":"refuse to write decrypted asset inside workspace","messagePattern":"refuse to write decrypted asset inside workspace","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/api/file.go","lineNumber":56,"sourceCode":")\n\n// errMsgSeeKernelLog 接在 API 错误提示末尾，引导用户查看内核日志以获取完整信息（避免在 Msg 暴露工作空间绝对路径）。\nconst errMsgSeeKernelLog = \". For details, see the SiYuan kernel log.\"\n\n// rejectEncryptedBoxPath 检查 absPath 是否落在加密笔记本目录下（含 symlink 绕过），是则返回 true。\n// 原始文件 API（getFile/putFile/copyFile/renameFile/removeFile）是绕过加密层的逃生口，\n// 对加密笔记本的任何文件读写都应拒绝——合法读写走专用 API（upload/getBlockKramdown 等，已加密感知），\n// 避免密文泄漏给插件或明文破坏加密格式。\n// 防止 symlink 绕过：找到最长已存在的父路径，解析 symlink 后拼回剩余路径，再检查是否落入加密 box。\nfunc rejectEncryptedBoxPath(absPath string) bool {\n\treturn model.EncryptedRawPathBoxID(absPath) != \"\"\n}\n\n// copyDecryptedAsset 将加密 asset 解密后复制到目标路径（dest 必须在工作区外）。\nfunc copyDecryptedAsset(src, dest string) error {\n\t// 安全守卫：dest 必须在工作区外，防止解密后的明文落入工作区普通目录\n\tif gulu.File.IsSubPath(util.WorkspaceDir, dest) {\n\t\treturn fmt.Errorf(\"refuse to write decrypted asset inside workspace\")\n\t}\n\tboxID := model.ExtractBoxIDFromAssetsPath(src)\n\tif boxID == \"\" || !model.IsEncryptedBox(boxID) {\n\t\treturn fmt.Errorf(\"source is not an encrypted asset\")\n\t}\n\tmodel.HoldBoxReadLock(boxID)\n\tdefer model.ReleaseBoxReadLock(boxID)\n\tdek, dekErr := model.GetDEKIfUnlocked(boxID)\n\tif dekErr != nil {\n\t\treturn dekErr\n\t}\n\tdiskName := filepath.Base(src)\n\tdata, readErr := os.ReadFile(src)\n\tif readErr != nil {\n\t\treturn readErr\n\t}\n\tplain, decErr := model.DecryptAsset(boxID, diskName, dek, data)\n\tif decErr != nil {","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/api/file.go#L38-L74","documentation":"Returned by copyDecryptedAsset (file.go:56) as a deliberate security guard: the destination path for a decrypted asset falls inside the workspace directory. Because the workspace contains notebooks (including encrypted ones), writing decrypted plaintext there would risk leaking cleartext into a location that could be read by raw-file APIs or synced — so the guard refuses it unconditionally via gulu.File.IsSubPath(util.WorkspaceDir, dest).","triggerScenarios":"An internal/programmatic call to copyDecryptedAsset(src, dest) where `dest` is computed to be inside util.WorkspaceDir (e.g. exporting an encrypted asset into a non-encrypted asset folder, or a plugin-supplied destination under the workspace). The IsSubPath check at file.go:55 triggers before any decryption occurs.","commonSituations":"Plugin or automation attempting to 'restore' a decrypted asset into the workspace. Misconfigured export destination defaulting to a workspace-relative path. A code path that reuses workspace-relative asset paths as copy targets.","solutions":["Choose a destination outside the workspace directory (an absolute path under the OS temp dir or a user-selected export folder).","If the user genuinely needs the cleartext inside SiYuan, decrypt-and-reimport through the proper flow rather than writing raw plaintext into the workspace.","Validate the destination with gulu.File.IsSubPath(util.WorkspaceDir, dest) and reject before calling the API."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Ensure destination is outside the workspace before copying a decrypted asset\nif isSubPath(util.WorkspaceDir, dest) {\n  dest = filepath.Join(os.TempDir(), filepath.Base(dest))\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always choose decrypted-asset destinations under the OS temp dir or a user export folder, never the workspace.","Reject workspace-relative destination input in plugin UI.","Document that decrypted plaintext must not re-enter the workspace."],"tags":["encryption","security","filesystem","kernel"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}