← Community
bugfixed

/api/policy/apply-scenario replaces the live approval policy with only a session token, bypassing dual-control approve

ShwetaShweta#115d ago · 71 views
affected: station-v0.80fixed in: station-v0.83

This codebase's own policy-routing file states: "a policy change is one
of the highest-consequence writes in Studio, so /api/policy/approve
dual-controls (session + approve code)." That is true for
/api/policy/approve. It is not true for /api/policy/apply-scenario,
which reaches the exact same commit function and requires only a normal
session token.

Both routes ultimately call primitives.approval_policy.approve_change()
(via policy_endpoints.handle_approve()) -- the function that bumps the
policy version, signs it, and writes it to disk as the new live policy,
with no gate of its own; the dual-control check exists only at the HTTP
route level:

_handle_policy_approve() (the "real" approve endpoint):
if not handler._require_session(): return
if not handler._require_approve(): # dual-control
return
return handler._send(200, policy_endpoints.handle_approve(WS, body))

_handle_policy_apply_scenario() (apply a named policy template):
if not handler._require_session(): return
...
staged = policy_endpoints.handle_stage(WS, {"rules": new_rules})
approved = policy_endpoints.handle_approve(WS, {"staging_id": ...})
# no _require_approve() call anywhere in this function

Reproduction steps:

  1. Extract a clean station-v0.80 tarball, sys.path.insert(0, "workbench").
  2. Build a handler stub that implements _require_session() (returning

True) but has NO _require_approve() method at all -- if the code path
ever tried to call it, this raises AttributeError instead of silently
succeeding, so a clean run to completion is itself the proof no
dual-control check happens.

  1. Call routes.dispatch_policy._handle_policy_apply_scenario(

{"scenario_id": "solo_developer"}, handler) -- any of the built-in
scenario ids reproduces this.

  1. Read the resulting WS/approval_policy.json.

Expected: applying a scenario is refused, or itself requires the same
dual-control second credential /api/policy/approve requires, since it
performs the identical commit.

Actual: the handler stub runs to completion with ok: true. The station's
live approval_policy.json is replaced and committed -- version bumped,
newly signed, written to disk as the governing policy for every future
action on the station -- with only a session token and no approve
credential anywhere in the call path.

Root cause: workbench/routes/dispatch_policy.py
_handle_policy_apply_scenario() calls
policy_endpoints.handle_approve() directly to finalize the scenario
swap, without the handler._require_approve() check its sibling
_handle_policy_approve() performs before calling the identical
function. primitives/approval_policy.py approve_change() (what
handle_approve() wraps) has no internal caller-identity check of its
own -- the dual-control guarantee exists solely at the route layer, and
only one of the two routes that can trigger it has it.

Suggested fix: require handler._require_approve() in
_handle_policy_apply_scenario() before calling
policy_endpoints.handle_approve(), exactly as _handle_policy_approve()
already does -- or route apply-scenario through a stage-only step and
require the operator to hit the existing /api/policy/approve endpoint
to finalize it, so there is exactly one code path that can ever commit a
policy change.

5 pts

0 replies

Sign in to reply.