MCP: bind Claude permission replies to originating session#56396
MCP: bind Claude permission replies to originating session#56396lifelikezzh wants to merge 5 commits into
Conversation
…ge-vulnerability MCP: bind Claude permission replies to originating session
Greptile SummaryThis PR closes a permission-spoofing vulnerability in the MCP channel by binding each pending Claude permission request to the
Confidence Score: 5/5Safe 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.
|
| 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
| method: z.literal("notifications/claude/channel/permission_request"), | ||
| params: z.object({ | ||
| request_id: z.string(), | ||
| session_key: z.string(), |
There was a problem hiding this 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.
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.There was a problem hiding this comment.
💡 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".
|
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. |
…-for-phone-control phone-control: require operator.admin for arm/disarm across channels
There was a problem hiding this comment.
💡 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")) { |
There was a problem hiding this comment.
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 👍 / 👎.
Motivation
yes/no <id>) could be spoofed from any session because pending requests were keyed only by a shortrequest_id.Description
session_keyin thenotifications/claude/channel/permission_requestschema and propagate it through the MCP server; changedsrc/mcp/channel-shared.tsandsrc/mcp/channel-server.ts.sessionKeyand only acceptyes/no <request_id>replies when the inbound message'ssessionKeymatches; changedsrc/mcp/channel-bridge.ts.src/mcp/channel-server.test.ts.Testing
pnpm test -- src/mcp/channel-server.test.ts, which passed (1 passed, 6 tests).pnpm check, which completed successfully (lint/type checks passed).Codex Task