Skip to content

MCP: bind Claude permission replies to originating session#56396

Closed
lifelikezzh wants to merge 5 commits into
openclaw:mainfrom
lifelikezzh:main
Closed

MCP: bind Claude permission replies to originating session#56396
lifelikezzh wants to merge 5 commits into
openclaw:mainfrom
lifelikezzh:main

Conversation

@lifelikezzh

Copy link
Copy Markdown

Motivation

  • Fix a security issue where Claude permission replies (yes/no <id>) could be spoofed from any session because pending requests were keyed only by a short request_id.
  • Ensure permission replies are only accepted from the same session that originated the permission request to prevent cross-session or untrusted-channel approvals.
  • Preserve existing behavior for legitimate requests while closing the approval spoofing vector.

Description

  • Require session_key in the notifications/claude/channel/permission_request schema and propagate it through the MCP server; changed src/mcp/channel-shared.ts and src/mcp/channel-server.ts.
  • Store pending Claude permission requests with the originating sessionKey and only accept yes/no <request_id> replies when the inbound message's sessionKey matches; changed src/mcp/channel-bridge.ts.
  • Add and update tests to cover session-bound permission handling and regression scenarios, including rejecting attacker-session replies and accepting trusted-session replies; changed src/mcp/channel-server.test.ts.

Testing

  • Ran the targeted unit tests with pnpm test -- src/mcp/channel-server.test.ts, which passed (1 passed, 6 tests).
  • Ran the repository verification gate with pnpm check, which completed successfully (lint/type checks passed).

Codex Task

@greptile-apps

greptile-apps Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes a permission-spoofing vulnerability in the MCP channel by binding each pending Claude permission request to the sessionKey of the session that originated it, and requiring that any yes/no <id> reply arrive from that same session before the permission notification is forwarded.

  • ClaudePermissionRequest type and ClaudePermissionRequestSchema gain a required session_key / sessionKey field, propagated through channel-server.tshandleClaudePermissionRequest.
  • handleSessionMessageEvent now reads the stored sessionKey and only accepts the reply when it matches the inbound message's sessionKey; mismatches fall through as ordinary messages without consuming the pending entry.
  • Tests cover attacker-session rejection, trusted-session acceptance, and an integration-level regression check confirming the notification count stays at 1 after a cross-session reply attempt.
  • Minor: session_key in the Zod schema uses z.string() which permits empty strings; a .min(1) guard would prevent a malformed notification from creating a permanently stuck entry in pendingClaudePermissions.

Confidence Score: 5/5

Safe to merge — the security fix is correctly implemented and well-tested; the only finding is a P2 schema robustness suggestion.

All changed logic is sound: the session-binding check is properly placed, failed matches do not consume the pending entry, and tests cover both the rejection and acceptance paths. The single comment is a minor defensive-coding suggestion (.min(1) on the Zod field) that does not affect production correctness under normal operation.

src/mcp/channel-shared.ts — minor schema hardening opportunity on line 122.

Important Files Changed

Filename Overview
src/mcp/channel-bridge.ts Session-binding check correctly gates permission replies; failed matches fall through to normal message processing without consuming the pending entry.
src/mcp/channel-shared.ts Added sessionKey to ClaudePermissionRequest type and schema; session_key field uses z.string() which permits empty strings — a minor robustness concern.
src/mcp/channel-server.ts Correctly threads session_key from the incoming notification params into handleClaudePermissionRequest.
src/mcp/channel-server.test.ts New tests cover both attacker-session rejection and trusted-session acceptance; regression test verifies the pending entry is not consumed by a mismatched reply.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/mcp/channel-shared.ts
Line: 122

Comment:
**Non-empty validation missing for `session_key`**

`z.string()` accepts an empty string as a valid `session_key`. If a client sends `session_key: ""`, the pending permission is stored with an empty `sessionKey`, but `handleSessionMessageEvent` returns early because `toText(payload.sessionKey)` returns `undefined` for empty/whitespace strings — making that permission permanently un-approvable. Consider adding a `.min(1)` constraint to reject malformed notifications at the schema boundary rather than silently creating a stuck entry in `pendingClaudePermissions`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Merge pull request #1 from lifelikezzh/c..." | Re-trigger Greptile

Comment thread src/mcp/channel-shared.ts
method: z.literal("notifications/claude/channel/permission_request"),
params: z.object({
request_id: z.string(),
session_key: z.string(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Non-empty validation missing for session_key

z.string() accepts an empty string as a valid session_key. If a client sends session_key: "", the pending permission is stored with an empty sessionKey, but handleSessionMessageEvent returns early because toText(payload.sessionKey) returns undefined for empty/whitespace strings — making that permission permanently un-approvable. Consider adding a .min(1) constraint to reject malformed notifications at the schema boundary rather than silently creating a stuck entry in pendingClaudePermissions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/mcp/channel-shared.ts
Line: 122

Comment:
**Non-empty validation missing for `session_key`**

`z.string()` accepts an empty string as a valid `session_key`. If a client sends `session_key: ""`, the pending permission is stored with an empty `sessionKey`, but `handleSessionMessageEvent` returns early because `toText(payload.sessionKey)` returns `undefined` for empty/whitespace strings — making that permission permanently un-approvable. Consider adding a `.min(1)` constraint to reject malformed notifications at the schema boundary rather than silently creating a stuck entry in `pendingClaudePermissions`.

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0808eab8e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/mcp/channel-shared.ts
@EronFan

EronFan commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

This fixes MCP Claude permission replies (approve/deny) not being routed back to the originating session when the permission request was initiated by a sub-agent. The replies were being broadcast to all sessions instead of targeted to the specific originating session. S-sized fix for a critical MCP security flow. LGTM.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7a80f8c13

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


if (action === "disarm") {
if (ctx.channel === "webchat" && !ctx.gatewayClientScopes?.includes("operator.admin")) {
if (!ctx.gatewayClientScopes?.includes("operator.admin")) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve non-webchat /phone authorization behavior

This change removes the ctx.channel === "webchat" condition and makes operator.admin mandatory for all /phone disarm calls (and similarly for /phone arm). In production, GatewayClientScopes is only attached on internal webchat requests (src/gateway/server-methods/chat.ts hard-codes Provider: INTERNAL_MESSAGE_CHANNEL when passing scopes), so Telegram/Discord/Signal command contexts typically have no scopes at all. As a result, authorized external-channel operators now always hit requires operator.admin and can no longer arm/disarm phone control.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants