← Community
bugopen

Policy validator accepts a wildcard '*' block rule and signs it, but evaluate() ignores '*' — a signed block-all rule is a silent no-op

marcofgvmarcofgv#236d ago · 91 views
affected: station-v0.93

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 accepts connector:"*" or verb:"*" verbatim and persists them. Contrast the auto_approve validator (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) is if 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_rulesevaluate, 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_rules strips+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_human lists accept a connector "*" (note a verb "*" is supported by no list — so a verb:"*" 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.

0 replies

Sign in to reply.