siyuan-note/siyuan · error
refuse to write decrypted asset inside workspace
Error message
refuse to write decrypted asset inside workspace
What it means
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).
Source
Thrown at kernel/api/file.go:56
)
// errMsgSeeKernelLog 接在 API 错误提示末尾,引导用户查看内核日志以获取完整信息(避免在 Msg 暴露工作空间绝对路径)。
const errMsgSeeKernelLog = ". For details, see the SiYuan kernel log."
// rejectEncryptedBoxPath 检查 absPath 是否落在加密笔记本目录下(含 symlink 绕过),是则返回 true。
// 原始文件 API(getFile/putFile/copyFile/renameFile/removeFile)是绕过加密层的逃生口,
// 对加密笔记本的任何文件读写都应拒绝——合法读写走专用 API(upload/getBlockKramdown 等,已加密感知),
// 避免密文泄漏给插件或明文破坏加密格式。
// 防止 symlink 绕过:找到最长已存在的父路径,解析 symlink 后拼回剩余路径,再检查是否落入加密 box。
func rejectEncryptedBoxPath(absPath string) bool {
return model.EncryptedRawPathBoxID(absPath) != ""
}
// copyDecryptedAsset 将加密 asset 解密后复制到目标路径(dest 必须在工作区外)。
func copyDecryptedAsset(src, dest string) error {
// 安全守卫:dest 必须在工作区外,防止解密后的明文落入工作区普通目录
if gulu.File.IsSubPath(util.WorkspaceDir, dest) {
return fmt.Errorf("refuse to write decrypted asset inside workspace")
}
boxID := model.ExtractBoxIDFromAssetsPath(src)
if boxID == "" || !model.IsEncryptedBox(boxID) {
return fmt.Errorf("source is not an encrypted asset")
}
model.HoldBoxReadLock(boxID)
defer model.ReleaseBoxReadLock(boxID)
dek, dekErr := model.GetDEKIfUnlocked(boxID)
if dekErr != nil {
return dekErr
}
diskName := filepath.Base(src)
data, readErr := os.ReadFile(src)
if readErr != nil {
return readErr
}
plain, decErr := model.DecryptAsset(boxID, diskName, dek, data)
if decErr != nil {View on GitHub (pinned to 251596fc0d)
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.
Defensive patterns
Strategy: validation
Validate before calling
// Ensure destination is outside the workspace before copying a decrypted asset
if isSubPath(util.WorkspaceDir, dest) {
dest = filepath.Join(os.TempDir(), filepath.Base(dest))
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- source is not an encrypted asset
- encrypted notebook is locked, please unlock it first
- import path is not sub path of import dir
- export source [%s] is not a regular file
- invalid import token
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/ac4c8821820b1b64.
Report an issue: GitHub.