At a glance
- Controls
- OWASP API4:2023 · SOC 2 CC7.2 · ISO 27001 A.12.1
Availability rarely fails to a flood of traffic; it fails to a single request that asks for far more work than it costs to make. A search with no upper bound, a report that scans everything, an upload with no size limit, a regular expression that backtracks, a nested payload that explodes when parsed. One request, cheap to send, can hold a worker, exhaust memory or lock a table long enough to stall the product for everyone else.
Why it happens
Inputs are validated for correctness, not for cost. A field checks that a number is a number, and never that it is below a sane maximum. Pagination defaults to a reasonable page size and lets the caller ask for a million. The expensive operation was written for the sizes seen in testing, and the size limit was never made explicit because ordinary use never reaches it.
How the attack unfolds
An attacker looks for the operation whose cost they control: the search that accepts any query, the export that has no row cap, the endpoint that parses whatever it is given. They send the largest, deepest, most expensive version the endpoint will accept, and repeat it just often enough to keep the workers busy. The product does not crash dramatically; it degrades, requests time out, and legitimate users are told to try again later.
It took two packets to crash a host that had been reviewed for twenty-seven years.The Record · OpenBSD, April 2026
Why review misses it
Every normal input works, so review and tests pass. The failure needs an input larger or deeper than anyone thought to try, and cost is not something a diff shows. The endpoint is correct for the sizes it was tested with, and silent about every size beyond them.
Where it hides
What a reviewer can look for in the code and in the behaviour, before anyone proves it.
- An input has no documented, server-enforced upper bound on size, length, depth or page count.
- A search, export or report can scan an unbounded amount of data on one request.
- User-supplied text reaches a regular expression that can backtrack, or a parser that expands nested input.
- A single request can hold a worker or a database lock long enough to affect other users.
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 each operation whose cost the caller controls: search, export, upload, parse, report.
- 02Send the largest, deepest and most expensive input the endpoint accepts, and measure the work it does.
- 03Repeat at a low rate to test whether a few requests can exhaust workers, memory or connections.
- 04Confirm that a documented ceiling exists and is enforced on the server, not only in the interface.
The evidence
- GET /search?q=*&limit=1000000200 · 22sFull scan holds a worker; others queue behind it
- POST /import (nested payload)200 · OOMParser expands the payload until the process runs out of memory
- GET /search?q=*&limit=1000000400 Bad RequestPage size capped server-side; query cost bounded
- POST /import (nested payload)413 Payload Too LargeSize and depth limits refuse the request before parsing
Every input must have a documented upper bound, enforced server-side, and no single request may exceed a fixed budget of compute.
Executable checkEach cost-bearing endpoint must reject inputs beyond its documented bound and stay within its compute budget, on every release.
Every rule MATT holds →What the fix looks like
- 01
Bound every input
Give each input a documented maximum size, length, depth and page count, enforced on the server.
- 02
Budget the work
Cap the compute, rows and time any single request may consume, and reject the ones that would exceed it.
- 03
Degrade, do not fall
Isolate expensive operations so one abusive request cannot take the whole product down with it.
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? →