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)
This commit is contained in:
kn1ghtc 2026-03-16 18:49:39 +08:00
parent 1cf544ffbc
commit fb35d46f9d

View File

@ -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) {