From fb35d46f9d9842aa85185a0c135dd6e94ff0db35 Mon Sep 17 00:00:00 2001 From: kn1ghtc <2888992+kn1ghtc@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:49:39 +0800 Subject: [PATCH] security: prevent self-approval in exec.approval.resolve handler A prompt-injected agent could call exec.approval.request to register a dangerous command and then immediately call exec.approval.resolve with "allow-once" to execute it, bypassing human-in-the-loop oversight. The ExecApprovalRecord already captures requestedByConnId on the request side, but the resolve handler never checked it, leaving the field unused despite the aspirational comment "Used to prevent other clients from replaying an approval id". This commit enforces the separation: if the WebSocket connection that created the approval request is the same connection attempting to resolve it, the resolve is rejected with INVALID_REQUEST. Attack scenario blocked: 1. Malicious/compromised agent sends exec.approval.request (connId=A) 2. Same agent immediately sends exec.approval.resolve allow-once (connId=A) 3. Previously: command executed without human review 4. Now: INVALID_REQUEST "requester cannot approve their own exec request" Fixes: CRITICAL-02 (exec approval self-approval / cross-client hijacking) --- src/gateway/server-methods/exec-approval.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/gateway/server-methods/exec-approval.ts b/src/gateway/server-methods/exec-approval.ts index 81d479cbbd6..4f47960f825 100644 --- a/src/gateway/server-methods/exec-approval.ts +++ b/src/gateway/server-methods/exec-approval.ts @@ -312,6 +312,24 @@ export function createExecApprovalHandlers( } const approvalId = resolvedId.id; const snapshot = manager.getSnapshot(approvalId); + // Security: prevent self-approval — the connection that submitted the request + // cannot also resolve it. This blocks prompt-injected agents from immediately + // approving their own dangerous command requests without human oversight. + if ( + snapshot?.requestedByConnId != null && + client?.connId != null && + client.connId === snapshot.requestedByConnId + ) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + "requester cannot approve their own exec request", + ), + ); + return; + } const resolvedBy = client?.connect?.client?.displayName ?? client?.connect?.client?.id; const ok = manager.resolve(approvalId, decision, resolvedBy ?? null); if (!ok) {