Skip to content

upto sessions: spend cap not reserved before serving → concurrent requests bypass the cap (TOCTOU) #3

Description

@adambalogh

Summary

The "upto" session spend cap is checked at request entry and debited at request completion, with no reservation held across the request lifetime. Because LLM generation takes seconds, many concurrent requests on a single session all pass the entry check before any of them is charged, so the cap only stops a fraction of them — the rest are served for free.

Found while reviewing #1.

Where

  • python/x402/http/middleware/flask.py_handle_session_mode (entry check → _stream_session_response, no reservation), StreamingSessionResponse.close_accumulate_session_cost (charge at completion)
  • python/x402/session.py / python/x402/redis_session.pyadd_cost

Details

_handle_session_mode checks session.is_exhausted and then immediately begins streaming. No budget is reserved before _stream_session_response; the only debit happens in StreamingSessionResponse.close() after the response iterator is exhausted. The check-then-serve-then-charge window spans the entire generation.

Attack

  1. Open one session with a small cap.
  2. Fire N concurrent requests on that session.
  3. All N read is_exhausted == False at the gate (none charged yet) → all N stream in full.
  4. Only after each completes does add_cost run; the first few fit under the cap, the rest overflow and are dropped (see the companion "over-cap cost dropped" issue) → served free.

One signed session can thus serve roughly unlimited concurrent inference regardless of the cap. Redis's atomic _ADD_COST_SCRIPT does not help — it keeps the accumulator correct but cannot un-serve work already delivered.

Impact

Cap is not a real ceiling under concurrency; operator loss scales with the client's concurrency, not the signed amount. High severity, and it amplifies the over-cap-drop issue.

Suggested fixes

  • Reserve budget atomically before streaming: debit an estimate (e.g. max_tokens-worth, or a per-request floor) at entry, then reconcile to actual at close(). Reject at the gate when the reservation would exceed remaining budget.
  • And/or enforce a per-session in-flight concurrency limit so the outstanding un-reconciled exposure is bounded.

Notes

Pre-existing architectural issue, not introduced by #1. Companion to the over-cap-cost-dropped issue; a pre-serve reservation scheme would address both together.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions