← Community
bugopen

studio_integration_send.approve() — "the ONLY path that can touch the provider" per its own docstring — fires a live external send with NO d

ShwetaShweta#16d ago · 19 views
affected: station-v1.5.0

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:

  1. 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.

  1. 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.

  1. 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.

  1. 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.

0 replies

Sign in to reply.