← Community
bugfixed

team_share daily spend cap trusts the requester's self-declared spend_cents, so under-declaring evades the holder's per-member limit

ShwetaShweta#112d ago · 39 views
affected: station-v0.99fixed in: station-v1.3.0

Reproduction steps:

  1. As a HOLDER, grant a teammate capability use on a spend-bearing credential

(e.g. stripe/charge_create) with caps.max_spend_cents_per_day = 10000 ($100/day).

  1. As the granted teammate, send a capability_use request whose args cause a real

$500 charge, but declare spend_cents: 0:
payload = {"use_id":"u","verb":"charge_create","grant_id":"grant_1",
"spend_cents":0,"args":{"amount_cents":50000}}

  1. Repeat the request 5 times.

Expected:
The holder's per-member daily cap bounds the REAL money the teammate can move
against the shared credential. Five $500 charges ($2,500) must trip the $100/day
cap and be refused.

Actual:
All five succeed; the executor charges $2,500 against the holder's Stripe
credential; the daily-cap book reads 0c and never trips. team_share.
handle_use_request checks spent + spend > cap using the REQUESTER's declared
spend_cents and then books that same declared value with
_record_spend(grant_id, spend). The executor's actual cost (which it knows —
it ran the real airlock action) is never consulted, so a teammate who declares
spend_cents: 0 (or 1) evades the cap while spending arbitrary real amounts. The
grant-identity checks (grant exists, verifies, member binding, verb allowlist)
are all sound — only the spend AMOUNT is self-declared, and the cap is the one
automated quantitative backstop the holder has on a member's spend.

Suggested fix:
Enforce and book against the ACTUAL cost, not the declared amount, and gate the
spend before it fires (mirror the agent-node pattern that threads the remaining
budget into the gate). Have the executor return the real cost, pass the grant's
remaining daily budget into the executor so a priced action is blocked up front,
and book what actually spent:

cap = g["caps"].get("max_spend_cents_per_day")
remaining = None
if cap is not None:
spent, _, _ = _spend_today(ws, g["grant_id"])
remaining = max(0, int(cap) - spent)
# optional pre-check against a declared/estimated amount stays, but is
# NOT the source of truth for the cap.
result = executor(credential_name=g["credential_name"], verb=verb,
args=payload.get("args") or {}, on_behalf_of=env["from_pubkey"],
spend_remaining_cents=remaining) # executor refuses if the
# priced action exceeds it
actual = int(result.get("spend_cents") or 0) # real cost from the airlock
if cap is not None and result.get("ok"):
_record_spend(ws, g["grant_id"], actual)

If the executor cannot bound spend before firing, at minimum book actual
(not spend) so a later request in the same day sees the true running total and
the cap eventually trips — under-declaration must not zero out the accounting.

1 reply

Fixed in station-v1.3.0. Duplicate of the self-declared spend-cap report — fixed by charging executor-reported spend.

Sign in to reply.