At a glance
- Controls
- OWASP API4:2023 · SOC 2 CC6.1 · ISO 27001 A.12.1
Every product has a unit that costs real money to serve. For AI features that unit is a model call, and it can cost a fraction of a cent or several cents each. The product meters that call inside the flow the user is meant to take. The trouble starts when the same capability is reachable by a route that skips the meter: a raw endpoint, an internal service exposed to the internet, or a prompt field that will answer anything.
Why it happens
Metering, quotas and billing are usually built around the intended path, because that is the path the team designed and demoed. The endpoint that actually does the work sits one layer beneath, and it does not know or care whether the meter above it ran. If that endpoint can be called directly, the accounting is optional. Free trials make it worse, because the cheapest way to get more free capacity is to hold more free accounts.
How the attack unfolds
An attacker finds the endpoint that performs the expensive operation and calls it in a loop, with a larger context, a longer output or a more expensive model than the interface would ever request. They spread the load across many trial accounts so no single one trips a limit. The product keeps serving, because each individual call looks like ordinary use, and the cost surfaces only when the invoice arrives.
The limit was on the button, not on the thing the button called.MATT research team
Why review misses it
The metered path works perfectly in every test, because the tests use the interface. The unmetered path is the same code reached a different way, and nobody writes a test that calls it out of context. The cost of the gap is invisible until someone is paying it.
Where it hides
What a reviewer can look for in the code and in the behaviour, before anyone proves it.
- The endpoint that performs an expensive operation can be called without the product flow that meters it.
- Model choice, context size or output length are taken from the request without a server-side ceiling.
- Quotas are enforced per account but a single actor can hold many trial accounts.
- There is no alert that fires within minutes when spend for a user or key jumps.
A real instance
Details altered to protect the customer. Pattern, timing and outcome are as found.
How MATT tests it
The proof runs against the deployed product, on every release, whoever wrote the code.
- 01Find the endpoint behind each metered or AI feature and call it directly, outside the interface.
- 02Push the expensive parameters: larger context, longer output, a more costly model than the interface offers.
- 03Distribute the calls across many accounts and keys to test whether limits hold under spread.
- 04Measure how quickly a spend anomaly is noticed, from first abusive call to a human being paged.
The evidence
- POST /internal/generate (no product flow)200 OKRaw model call served, no quota consulted
- x200 across 40 trial accounts200 OKPer-account limits never tripped; bill climbs
- POST /internal/generate (no product flow)403 ForbiddenEndpoint reachable only through the metered flow
- x200 across 40 trial accounts429 Too Many RequestsSpend ceiling per actor holds, anomaly pages a human
A metered operation must be reachable only through the flow that meters it, and no account may spend beyond its plan by any route.
Executable checkEvery expensive endpoint must reject a call made outside its metered flow, and a spend anomaly must page a human within fifteen minutes, on every release.
Every rule MATT holds →What the fix looks like
- 01
Meter at the expensive call
Enforce the quota where the cost is incurred, so no route can reach the operation without accounting for it.
- 02
Bound the parameters
Cap model, context size and output length server-side, so a request cannot ask for an arbitrarily expensive answer.
- 03
Alert on spend, not just usage
A spend anomaly for any user or key must page a human within fifteen minutes.
A check that was never written is found by proving the deployed behaviour, fixed with the engineer who shipped it, and kept fixed by running the control on every change. What is ExploitOps? →