← Community
bugfixed

Escalation-approval intake accepts a request from any signer, with no relationship or allowlist check on the sender

ShwetaShweta#137d ago · 46 views
affected: station-v0.71fixed in: station-v0.74

eproduction steps:

  1. Extract a clean station-v0.71 tarball, sys.path.insert(0, "workbench").
  2. import ed25519_pure as ed; from primitives import approval_router as AR
  3. Generate two independent Ed25519 keypairs with os.urandom(32) +

ed.publickey_from_seed(seed): one representing the target approver's
station, one representing a total stranger who has never interacted
with that station before.

  1. As the stranger, build an approval_request with

AR.build_request(requester_pubkey=<stranger's own pubkey>,
approver_pubkey=<target approver's pubkey>, action={"command_id":
"stripe.create_refund", "inputs": {"amount_cents": 9999900}},
reason_for_escalation="URGENT: customer escalation, approve
immediately"). Sign it with the stranger's own seed -- a completely
genuine signature, since they are only vouching for themselves.

  1. Deliver it exactly the way routes/relay.py's kind-dispatcher does for

kind="approval_request": AR.receive_incoming(target_ws, {"kind":
"approval_request", "body": forged_request}).

  1. Check the target station's pending-approval queue with

AR.list_pending(target_ws).

Expected: a station should only accept an escalation request into its
local human-facing queue from a sender it has some established
relationship with (team membership, a configured allowlist, or similar) --
not from an arbitrary, never-seen-before keypair.

Actual:
receive_incoming() accepted the request from a total stranger: True
pending queue entries before/after: 0 -> 1
The crafted request -- including an attacker-chosen action, dollar amount,
and urgency-framed reason_for_escalation text -- lands directly in the
target station's WS/approval_pending/ directory and renders in Studio's
approval UI exactly like a legitimate internal escalation, with nothing to
distinguish it as coming from an unrelated, arbitrary sender.

Root cause: workbench/primitives/approval_router.py, receive_incoming()
(~line 202-222):

def receive_incoming(ws, event):
body = event.get("body") or {}
if body.get("schema") != REQUEST_SCHEMA:
return True
requester_pubkey = body.get("requester_pubkey") or ""
ok, note = _verify(body, requester_pubkey)
if not ok:
return True
p = os.path.join(_pending_dir(ws), body["request_id"] + ".json")
...

Exactly like the decision-forgery issue, _verify() genuinely proves the
document was signed by whoever holds the keypair it names -- the defect is
that nothing constrains WHO is allowed to be that signer in the first
place. A sender can always successfully "prove" they are themselves; the
missing check is authorization (is this sender someone this station should
be listening to at all), not authentication. There is no team-membership
check, no configured allowlist, and no rate limit on how many such
requests one unrelated sender can inject.

Suggested fix: before filing an incoming request into the local pending
queue, check the requester_pubkey against an explicit allowlist or
existing relationship (e.g. team membership, matching the Teams mesh's own
membership checks) and reject/quarantine requests from unrecognized
senders rather than filing them for human review indistinguishably from
legitimate ones. At minimum, surface the sender's trust status in the
Studio approval UI so a reviewer can tell an unvetted external request
apart from an internal one.

3 pts

1 reply

Verified: receive_incoming accepted an approval_request from any signer with no allowlist. Now the request must name this station as the approver, and when an intake policy exists (allowlist file or team manifest) the requester must be listed; with no policy it is filed but tagged unverified for the human approver. Confirmed against the code and fixed on the v0.74 batch (verified + regression-tested); ships in station-v0.74. Thanks shweta.

Sign in to reply.