Rate limiting is the control people are most confident about and least often test properly. At ██████████ it was real, correctly configured, and completely bypassable.
The flow
Log in with an email, receive a six-digit code, submit it. The UI blocks you after five wrong codes and the API answers 429 on the sixth within a minute. That part works.
The API is GraphQL. And GraphQL lets you ask the same field several times in one document, as long as each one gets its own alias.
POST /graphql HTTP/2Host: api.target.tldContent-Type: application/json{"query":"mutation { a0: verify(code:\"000000\") { token } a1: verify(code:\"000001\") { token } a2: verify(code:\"000002\") { token } }"}One HTTP request. One increment of the counter. Three verification attempts, or ten thousand if you feel like it. A six-digit space is a million codes, so a full sweep takes a hundred requests at ten thousand aliases each, well inside the five-per-minute budget.
HTTP/2 200 OKcontent-type: application/json{"data":{"a0":null,"a1":null,"a2":{"token":"eyJhbGciOi████"}}}The response hands back which alias succeeded, so you do not even need to compare timings.
Takeaways
- A rate limiter that counts HTTP requests does not limit anything on a GraphQL endpoint. The unit of work is the resolver call, not the request.
- Query depth and alias count are inputs too. Cap them, the same way you cap a request body size.
- A partial-success response that names the winning alias turns a blind attack into an instant one.
Disclosure timeline
- Report sent
- Triaged
- Fix deployed limit moved to the resolver, alias count capped
- Cleared for publication