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

  1. Choose a destination outside the workspace directory (an absolute path under the OS temp dir or a user-selected export folder).
  2. If the user genuinely needs the cleartext inside SiYuan, decrypt-and-reimport through the proper flow rather than writing raw plaintext into the workspace.
  3. 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

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


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/ac4c8821820b1b64. Report an issue: GitHub.