siyuan-note/siyuan · error

access to sensitive workspace file is forbidden

Error message

access to sensitive workspace file is forbidden: %s

What it means

The MCP file tools share the HTTP file API's sensitive-file blacklist via util.IsForbiddenAbsPath. Paths such as conf/conf.json, data/snippets/conf.json, data/templates, and data/.siyuan/publishAccess.json are hard-blocked so the MCP server can never read or overwrite credentials, snippets config, templates, or publish authorization state. authorizePath returns this error after the workspace-containment and symlink checks pass.

Solutions

  1. Edit these files through the proper channels (kernel settings API, UI settings) instead of raw file access
  2. Exclude the sensitive paths (conf/conf.json, data/snippets/conf.json, data/templates, data/.siyuan/publishAccess.json) from bulk copy/traversal/extraction inputs
  3. If a backup is needed, use the kernel's own backup/export features rather than reading the raw file via MCP

Example fix

// before
copyMcpFile("/workspace/conf/conf.json", "/tmp/backup.json")
// after (use the config API instead)
conf := fetchKernelAPI("/api/system/getConf")
writeFile("/tmp/backup.json", conf)
Defensive patterns

Strategy: validation

Validate before calling

const SENSITIVE = ['conf/conf.json','data/snippets/conf.json','data/templates','data/.siyuan/publishAccess.json'];
const rel = path.relative(WORKSPACE_DIR, target);
if (SENSITIVE.some(s => rel === s || rel.startsWith(s + path.sep))) {
  throw new Error('target is a forbidden sensitive path');
}

Try / catch

try {
  await callMcpTool('readFile', { path: target });
} catch (e) {
  if (String(e.message).includes('sensitive workspace file is forbidden')) {
    // reroute to kernel settings API instead of raw file access
  }
}

Prevention

When it happens

Trigger: Any MCP file tool call (resolvePath, authorizeFinalPath, authorizeArchiveEntry) whose target — including an archive extraction destination or a recursive traversal descendant — is exactly one of the blacklisted sensitive paths or resides inside them (e.g. data/templates itself).

Common situations: Trying to back up or edit conf/conf.json through MCP file tools; bulk-copying data/ which sweeps in data/templates; extracting a zip whose entries target data/.siyuan/publishAccess.json; scripts that treat the whole workspace as readable.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/9ec997bd3fea3ddf. Report an issue: GitHub.

Appendix: source

Thrown at kernel/mcp/tools/file.go:116

// authorizePath 校验单个最终路径是否允许访问,display 仅用于错误信息:顶层调用传工作区相对路径,
// 递归遍历、目录拷贝和压缩包解压传最终路径本身。
func authorizePath(abs, display string) error {
	if !gulu.File.IsSubPath(util.WorkspaceDir, abs) {
		return fmt.Errorf("path escapes workspace: %s", display)
	}
	// 拒绝加密笔记本目录:MCP 文件工具不能读写加密 box 下的文件(防止密文泄漏或明文破坏加密格式)
	if boxID, encrypted := rejectEncryptedPath(abs); encrypted {
		return fmt.Errorf("path belongs to encrypted notebook [%s]: %s", boxID, display)
	}
	// 防止 symlink 逃逸工作区:解析符号链接后再次检查
	if resolved := util.ResolveLongestExistingParent(abs); resolved != abs && !gulu.File.IsSubPath(util.WorkspaceDir, resolved) {
		return fmt.Errorf("symlink escapes workspace: %s", display)
	}
	// 禁止访问敏感文件(conf/conf.json、data/snippets/conf.json、data/templates、data/.siyuan/publishAccess.json),
	// 与 HTTP 文件 API 共用同一黑名单(见 kernel/util/path_guard.go 的 IsForbiddenAbsPath)
	if util.IsForbiddenAbsPath(abs) {
		return fmt.Errorf("access to sensitive workspace file is forbidden: %s", display)
	}
	return nil
}

// authorizeFinalPath 对即将打开或创建的最终路径做授权。resolvePath 只覆盖调用方给出的路径,
// 容器路径合法不代表其后代合法:递归遍历、复制、解压、删除、重命名都必须对每一个后代路径再次调用本函数。
func authorizeFinalPath(abs string) error {
	return authorizePath(abs, abs)
}

// authorizeSubtree 校验路径及其全部后代,任一后代被拒绝即整体拒绝。删除和重命名是目录级操作,
// 只校验目录本身会让受保护的后代被删除或搬出黑名单范围(例如 file.delete("conf"))。
// 使用 Lstat:删除和重命名不会跟随符号链接,与 os.RemoveAll、os.Rename 的语义保持一致。
func authorizeSubtree(abs string) error {
	if err := authorizeFinalPath(abs); err != nil {
		return err
	}
	info, err := os.Lstat(abs)

View on GitHub (pinned to 9f775e8a12)