studio_integration_send.approve() — "the ONLY path that can touch the provider" per its own docstring — fires a live external send with NO dual-control check, even when the operator has dual-control enabled; a third, still-open instance of a bug class already fixed once on a sibling surface (v1.5.0)
Reproduction steps:
- studio_server.py's _require_approve() (line 7042) is the dual-control
gate: "when enabled in the security/policy panel, the most
consequential actions (approving a live send or a policy change) also
require the APPROVE CODE printed only to the launching terminal." It is
correctly wired into routes/requests.py's /api/requests/act (line 225),
routes/dispatch_policy.py's policy-change routes (lines 62, 149),
routes/dispatch_cap_off_wave3.py (line 990), routes/dispatch_misc.py
(line 157), routes/egress_approvals.py (line 64),
routes/approvals_incoming.py (line 56), and routes/team.py (lines 644,
694, 910) — seven other files, all correctly gating their own
"approving a live send"-class action on it.
- routes/dispatch_airlock.py's two approve handlers — _handle_integration_
airlock() (line 80, serving POST /api/integration/approve — "generic
registry-driven approve") and _handle_mcp_pending_approve() (line 119,
serving POST /api/mcp/pending/approve — "operator approve from
Sends-tab banner") — call handler._require_session() only (lines 85 and
131). Neither calls handler._require_approve() anywhere in the file.
Both ultimately call studio_integration_send.approve(), whose own
module docstring describes it as "the ONLY path that can touch the
provider" for this class of send.
- This is the SAME bug class already found and fixed once, on a
different pair of surfaces: c277e3 (dinkarshweta) — "dual-control...
was enforced on /api/requests/act's approve action but NOT on the
independently wired /api/commands/execute path." dispatch_airlock.py's
two approve routes are a third, independently-wired surface for the
identical action class that never received the same fix.
- Run repro_integration_approve_dual_control_bypass_v150.py against a
clean v1.5.0 extraction. It drives the REAL, unmodified
studio_integration_send.stage()/approve() and studio_server.
dual_control_required() against a real Ed25519 signing identity and a
real temp workspace with dual-control turned ON (writes
security_settings.json the same way set_dual_control(True) would).
It stages a legitimate, policy-approved, properly-signed send via the
real stage(), then calls the real approve() with the EXACT keyword
arguments dispatch_airlock.py's handlers pass — no approve-code, no
X-RailCall-Approve header, nothing dual-control-shaped anywhere in the
call:
dual_control_required() after enabling: True
stage() result: True staging_id: stg_385fcd430a2c16c4
dual-control is ON. Calling the REAL approve() with ZERO approve-code,
exactly as POST /api/integration/approve (session-gated only) does:
approve() result: {'ok': True, 'executed': True, ...,
'outcome': 'SENT', 'external_api_touched': True, ...,
'signature': {'alg': 'ed25519', ...}}
live provider apply() call count: 1
CONFIRMED
Expected:
With dual-control enabled, approving ANY live send — regardless of which
of the platform's several "approve" surfaces the operator (or a caller
holding only the session token) uses — must require the second APPROVE
CODE credential, exactly as /api/requests/act already does for the same
class of action.
Actual:
An operator (or anyone holding only the ordinary session token — e.g. a
sandboxed module that somehow obtained it, or any same-origin script)
fires a real, provider-touching send through
/api/integration/approve or /api/mcp/pending/approve with dual-control
completely unconsulted — the second credential the operator explicitly
turned on to require a human presenting BOTH the session token and the
terminal-only approve code is silently not checked on this surface,
identical in effect to the c277e3 gap fixed once already on
/api/commands/execute.
Suggested fix:
Add the same gate dispatch_cap_off_wave3.py and every other consequential-
action route already uses, in both handlers:
def _handle_integration_airlock(body, handler, path):
if not handler._require_session():
return
...
if path == "/api/integration/approve":
if not handler._require_approve():
return
...
def _handle_mcp_pending_approve(body, handler):
if not handler._require_session():
return
if not handler._require_approve():
return
...
Given this is now the THIRD independently-wired surface found missing
this exact check, consider moving the dual-control gate out of individual
route handlers and into studio_integration_send.approve() itself (or a
shared decorator/wrapper every live-send entry point must pass through),
so a fourth new approve surface cannot reintroduce the same gap by
omission.