This article explains how GraphQL batching can defeat rate limits designed to count HTTP requests rather than individual operations. By bundling multiple login attempts into a single POST request using aliases or array batching, an attacker can bypass login attempt throttling that caps five attempts per minute. The vulnerability exists because rate limiters often count requests as the unit of control, while GraphQL allows arbitrary numbers of operations within a single request.
- How GraphQL batching defeats request-based rate limitsGraphQL allows multiple operations to be bundled into one HTTP request through aliasing (the same field repeated under different names) or array batching (several operations in one payload). If a rate limiter counts requests rather than operations, a single POST containing 100 login mutations counts as one request, allowing an attacker to execute many password attempts while the counter registers only one.
- The GraphqlAPI lab demonstrates credential brute-force bypassThe lab presents a scenario where GraphqlAPI enforces a five-per-minute login limit per HTTP request. An attacker constructs a single GraphQL POST with many aliased login mutations, each trying a different candidate password for the admin account, sends it once, and inspects the batched response to identify which aliased mutation succeeded, effectively bypassing the rate limit.
- Attack requires confirming per-request rate limit designThe attack approach begins by confirming that the login rate limit is enforced per HTTP request, not per operation. Only systems that count requests rather than operations are vulnerable to this batching bypass technique.
- Fundamental design flaw in rate limit boundariesThe rate limit guards the wrong boundary by assuming one attempt per request, which holds true for traditional form-based login but fails for GraphQL where a single request carries an arbitrary number of operations. The counter never rises because the entire batch appears as one request to the limiter.
- Real-world impact on authentication and configurationBatching and alias-based rate-limit bypass enables credential brute-force and OTP bypass attacks. The vulnerability maps to Identification and Authentication Failures (A07) and Security Misconfiguration (A05) in the OWASP Top 10 (2021), and is well-known across many GraphQL frameworks that enable batching with naive per-request throttling.
- Defend by counting operations instead of requestsRate-limit per operation by counting every mutation or query inside a request rather than the request itself. Additionally, limit batch size and query depth or cost to reject requests bundling too many operations, apply account-specific throttling independent of transport, and consider disabling array batching if the API does not require it.
- Testing technique for rate limit verificationOn an authorized GraphQL target, test by aliasing a sensitive mutation many times in one request. If a per-request rate limit allows all attempts through, the throttling is counting requests rather than operations, confirming the vulnerability.
Bottom line: Rate-limit controls for GraphQL must count individual operations and apply throttling per operation, batch size, and query cost—not per HTTP request. Naive per-request rate limiting is fundamentally broken for GraphQL and enables attackers to execute credential brute-force and other attacks that the limit was designed to prevent.
📋 In this walkthrough
What this lab teaches
GraphQL can run multiple operations in a single HTTP request, either through aliasing (the same field many times under different names) or array batching (several operations in one payload). If a rate limit counts HTTP requests rather than operations, an attacker bundles many attempts into one request and sails past the limit.
The lesson: for GraphQL, the unit that matters is the operation, not the request. Controls that count requests are counting the wrong thing.
How GraphqlAPI rate-limits login
GraphqlAPI applies its five-per-minute limit per HTTP request. A single POST that contains many login mutations therefore counts as one against the limit, while actually trying many passwords — so a brute-force that should take ages runs in a handful of requests.
The attack approach
- Confirm the login rate limit is enforced per HTTP request, not per operation.
- Build a single GraphQL request that contains many login attempts (via aliases or batching).
- Each attempt tries a different candidate password for admin.
- Send the batch and read which aliased result succeeded.
Full walkthrough (spoiler)
If you still hit the limit, you are sending one attempt per request — batch them instead; open the steps.
Show the step-by-step solution
- Compose one GraphQL POST that includes many aliased login mutations, each trying a different candidate password for admin.
- Send it as a single request. The rate limiter counts it as one, so all attempts run.
- Inspect the batched response and find the aliased mutation that returned a successful login.
- Use that result to authenticate as admin. The lab detects the bypass and marks you solved.
Why it works
The rate limit guards the wrong boundary. It assumes one attempt per request, which is true for a normal form login but false for GraphQL, where a single request can carry an arbitrary number of operations. The counter never rises because, to it, the whole batch is one request.
Where this happens in the real world
Batching and alias-based rate-limit bypass is a well-known GraphQL weakness, because many frameworks enable batching and apply naive per-request throttling. It commonly enables credential brute-force and OTP bypass, and maps to Identification and Authentication Failures (A07) and Security Misconfiguration (A05) in the OWASP Top 10 (2021).
How to defend against it
- Rate-limit per operation, counting every mutation or query inside a request, not the request itself.
- Limit batch size and query depth/cost, rejecting requests that bundle too many operations.
- Apply account- and action-specific throttling (for example, login attempts per account) independent of transport.
- Consider disabling array batching if your API does not need it.
Try it, then go further
Reading about it is the easy part. Open the lab, try your own approach before peeking at the spoiler, and see how many attempts it takes.
