siyuan-note/siyuan · error

path escapes workspace: %s

Error message

path escapes workspace: %s

What it means

`resolvePath` joined the requested relative path onto `util.WorkspaceDir` and the result is no longer inside the workspace (failed `gulu.File.IsSubPath`). This is the primary directory-traversal guard for the MCP file tools: any path that resolves outside the workspace root is rejected.

Source

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

		return fileCopy(args)
	case "grep":
		return fileGrep(args)
	case "find":
		return fileFind(args)
	case "stat":
		return fileStat(args)
	}
	return CallToolResult{
		Content: []ContentItem{{Type: "text", Text: "unknown action '" + action + "', expected one of: [list, read, write, delete, rename, copy, grep, find, stat]"}},
		IsError: true,
	}, nil
}

func resolvePath(rel string) (string, error) {
	rel = filepath.Clean(strings.ReplaceAll(rel, "/", string(os.PathSeparator)))
	abs := filepath.Join(util.WorkspaceDir, rel)
	if !gulu.File.IsSubPath(util.WorkspaceDir, abs) {
		return "", fmt.Errorf("path escapes workspace: %s", rel)
	}
	// 拒绝加密笔记本目录:MCP 文件工具不能读写加密 box 下的文件(防止密文泄漏或明文破坏加密格式)
	if boxID, encrypted := rejectEncryptedPath(abs); encrypted {
		return "", fmt.Errorf("path belongs to encrypted notebook [%s]: %s", boxID, rel)
	}
	// 防止 symlink 逃逸工作区:解析符号链接后再次检查
	if resolved := util.ResolveLongestExistingParent(abs); resolved != abs && !gulu.File.IsSubPath(util.WorkspaceDir, resolved) {
		return "", fmt.Errorf("symlink escapes workspace: %s", rel)
	}
	// 禁止访问敏感文件(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", rel)
	}
	return abs, nil
}

// rejectEncryptedPath 检查路径是否属于加密笔记本(含 symlink 绕过),返回 boxID 和是否为加密 box。

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Pass paths relative to the workspace root and free of `..` segments that escape it.
  2. Avoid absolute paths unless they resolve inside the workspace.
  3. If the file legitimately lives outside the workspace, move/symlink it into the workspace first (the symlink itself is then re-checked).

Example fix

// before
{"path": "../../../etc/passwd"}
// after
{"path": "data/notebooks/20240101/notes.md"}
Defensive patterns

Strategy: validation

Validate before calling

// Reject paths that escape the workspace before calling the file tool.
func safeWorkspacePath(rel string) (string, error) {
    abs := filepath.Join(util.WorkspaceDir, filepath.Clean(strings.ReplaceAll(rel, "/", string(os.PathSeparator))))
    if !gulu.File.IsSubPath(util.WorkspaceDir, abs) {
        return "", fmt.Errorf("path escapes workspace: %s", rel)
    }
    return abs, nil
}

Prevention

When it happens

Trigger: Calling a file tool with a `path` containing `..` segments that climb above the workspace root, an absolute path to an external location, or any value that `filepath.Join` resolves outside `WorkspaceDir`.

Common situations: Passing `../../etc/passwd` or `/etc/passwd`. Using an absolute path (`/home/user/secret`) instead of a workspace-relative one. A leading `/` that `filepath.Join` treats as absolute and discards the base for.

Related errors


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