Affected: station-v0.93 (current)
Station: verified on station-v0.93 (current). File: primitives/approval_policy.py — _validate_rules() (328-333) vs evaluate() block matcher (363-365); reached wherever the approval policy is evaluated (the static DAG effect path and the connector airlock via studio_server.policy_gate; the agent path via agent_gate's own policy step). Class: incorrect authorization / policy validate-vs-enforce mismatch (CWE-863).
The defect
The schema documents block as requiring a named connector and verb (only auto_approve documents a "*" connector). The defect is that the block validator accepts a "*" value it cannot enforce — a no-op — instead of rejecting it, while the auto_approve validator is wildcard-aware:
- Validator accepts
"*"._validate_rules()for a block rule (328-333) only checks both fields are non-empty and normalizes case/whitespace; it acceptsconnector:"*"orverb:"*"verbatim and persists them. Contrast theauto_approvevalidator (283-291), which is wildcard-aware and rejects a verb-scoped"*"("a verb-scoped auto_approve rule needs a named connector, not *"). The block validator has no such awareness. - Evaluator ignores
"*".evaluate()'s block loop (363-365) isif r["connector"] == conn and r["verb"] == verb— a literal compare with no"*"handling. So a persisted"*"block never equals a real connector/verb and never fires.
Result: an operator writes a natural broad block — {"connector":"*","verb":"charge_create"} (block that action on every connector) or {"connector":"stripe","verb":"*"} (block every stripe verb) — the airlock validates it, persists it, signs it into the policy (on a signing-enabled install), and shows it in the block list, but evaluate() silently enforces nothing. The wildcard block rule itself never fires — the action takes the next-strictest decision (the require_human default, or an auto_approve if a broad reversible/compensable rule exists) unless some other, exactly-matching block rule applies.
Proof (container, real _validate_rules → evaluate, v0.93; real registry action class)
real stripe action: ('stripe', 'charge_create', 'compensable')
_validate_rules ACCEPTED block {'*','charge_create'} -> persisted: [{'connector':'*','verb':'charge_create'}]
_validate_rules ACCEPTED block {'stripe','*'} -> persisted: [{'connector':'stripe','verb':'*'}]
evaluate(stripe.charge_create) under signed block{'*','charge_create'} -> require_human (expected 'block')
auto_approve validation is '*'-aware -> REJECTS {'*',verb}: "a verb-scoped auto_approve rule needs a named connector, not *"
The block rule passes validation and is signed, yet the money-mover it names resolves to require_human instead of block.
Impact
The operator's explicit, signed prohibition is silently not enforced. The signed policy and its approval-card preview show a block that does not apply, so the operator believes an action is forbidden when it is not. Whether the action then lands on require_human (still a human, but not the intended hard stop) or auto_approve (fires with no human) depends on the other rules — but in every case the block the operator authored, validated, and signed does nothing.
Honest scope (corrected after adversarial review)
- This is not a case/whitespace bypass:
_validate_rulesstrips+lowercases block fields at persistence, so case/whitespace variance does not survive the authoring path. The defect is specifically the"*"wildcard being accepted by the validator but ignored by the evaluator. - It requires the operator to author a wildcard block rule — a plausible mistake because the sibling
auto_approve/require_humanlists accept a connector"*"(note a verb"*"is supported by no list — so averb:"*"block is doubly a no-op), but it is a policy-authoring / validate-vs-enforce mismatch, not a remote/unauthenticated bypass. - Not claiming a specific unattended charge fired; the always-true consequence is that a signed
"*"block yields a non-block decision.
Fix
Have _validate_rules reject a "*"/wildcard value in a block rule at authoring time (as the auto_approve validator already rejects a verb-scoped "*"), so a no-op block can never be persisted or signed — or, if wildcard blocks are intended, honor "*" in evaluate()'s block matcher. Either way the validator and the evaluator must agree. Surface any block rule that cannot match a real action in the approval-card policy preview.
Reviewed adversarially against the source before posting.