GraphQL Alias Batching: Brute-Force OTPs in One Request
Login MFA, password reset, and “verify email” flows often rate-limit HTTP requests, not GraphQL operations. One POST can carry dozens of aliased mutations that each try a different code. The WAF sees a single call. Your script sees parallel guesses.
I’ve seen 6-digit SMS OTPs fall in minutes when the API allowed 100+ aliases and returned distinct error payloads per alias.
Two batching styles
Aliases in one query/mutation document:
mutation {
a1: verifyOtp(code: "000001") { success }
a2: verifyOtp(code: "000002") { success }
# …
}
JSON array batching (some servers accept a list of operations in one body). Test both. Alias packing works even when array batching is disabled.
Finding the weak verifier
Intercept the legitimate OTP submit. Note argument names (code, token, otp), whether a challengeId is required, and how failures look. If every wrong code returns identical GraphQL errors, you still may distinguish success by data shape or HTTP 200 with success: true on one alias.
Check whether the mutation is behind the same rate limiter as REST /otp/verify. GraphQL gateways frequently are not.
Building a polite PoC
Stay inside program rules. Cap guesses. Prefer staging or accounts you own. Start with 10 aliases to prove the multiplier, then calculate theoretical attempts per minute for the report — do not demonstrate a full exhaust on production SMS.
Include:
- Raw body with ~20 aliases (trim for readability)
- Response where one alias returns success (or a timing/length delta if that is all you have)
- Comparison: 20 REST calls get blocked; 20 aliases in one call do not
Neighbor tricks
Batch password guesses on login if lockout is per-request. Batch invite-token checks. Batch “is email registered” queries for enumeration. Same root cause: operation-level controls missing behind a single HTTP envelope.
Introspection can reveal mutation names quickly; if introspection is off, harvest from the SPA’s persisted queries or webpack chunks.
What to recommend in the fix section
- Per-account and per-challenge rate limits counted per GraphQL operation / alias, not per HTTP request
- Exponential backoff and hard attempt caps on OTP challenges
- Short OTP validity and single-use codes
- Anomaly alerts on huge documents (depth, alias count, batch array size)
OWASP’s GraphQL cheat sheet and CWE-307 (improper restriction of excessive authentication attempts) frame the issue for triagers who do not live in GraphQL daily.
Severity notes
A bypass that multiplies throughput against 6-digit OTP is usually high when SMS/email codes gate account takeover. Four-digit codes are worse. If the challenge binds tightly to a high-entropy loginAttemptId and locks after five tries regardless of aliases, your finding shrinks — verify lockout still increments per alias before you file critical.
One request should never equal hundreds of password or OTP checks. If it does, GraphQL is doing the attacker’s scheduling for free.