Description
GraphQL Denial of Service encompasses a family of resource exhaustion attacks that exploit the inherent flexibility of the GraphQL query language. Unlike REST APIs where each endpoint returns a fixed data structure, GraphQL allows clients to specify arbitrary query depth, breadth, and complexity — a design feature that simultaneously enables powerful data fetching and creates a large attack surface for resource exhaustion.
CWE-770 — Allocation of Resources Without Limits or Throttling directly describes the failure mode: GraphQL servers that do not impose limits on query complexity, depth, or aliasing allow a single request to trigger unbounded resolver execution, database queries, and memory allocation.
Under A04:2021 — Insecure Design, the OWASP Top 10 highlights this as a design-level issue: the GraphQL server's default behaviour is to process whatever query it receives, and imposing appropriate limits is the application developer's responsibility — often one that is overlooked when moving quickly from REST to GraphQL.
How It Works
Deeply nested queries exploit circular or deeply linked schema relationships:
# If User → friends → User (circular relationship)
query DeepNest {
user(id: "1") {
friends {
friends {
friends {
friends {
friends {
id email
}
}
}
}
}
}
}
Each nesting level can multiply the number of database queries exponentially. A 10-level deep query on a user graph with 100 friends per user theoretically requires 100^10 database lookups.
Field duplication via aliases — GraphQL allows aliasing the same field multiple times in a single query. Without a cost limit, this executes the same expensive resolver thousands of times:
query AliasBomb {
a1: expensiveReport { total }
a2: expensiveReport { total }
a3: expensiveReport { total }
# ... repeated 1000 times
a1000: expensiveReport { total }
}
Batched query attacks — many GraphQL implementations support query batching (sending multiple operations in a single HTTP request as a JSON array). Without per-batch limits, a single request can trigger thousands of operations:
[
{"query": "mutation { login(user: \"admin\", pass: \"password1\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password2\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password3\") { token } }"}
]
This bypasses per-request rate limits because all attempts arrive in a single HTTP request, and is commonly used to conduct brute-force attacks against authentication mutations.
Introspection as reconnaissance — while not a DoS vector itself, unrestricted __schema introspection exposes the full type system to attackers, making it trivial to construct maximally expensive queries:
{ __schema { types { name fields { name type { name kind ofType { name } } } } } }
Impact
- API service unavailability — a single deeply nested or alias-bomb query can saturate CPU, exhaust database connection pools, or consume all available memory.
- Cascading backend failures — GraphQL resolvers typically make database calls; a single expensive query can exhaust connection pool limits, causing failures for all concurrent users.
- Brute-force via batching — mutation batching enables credential brute-forcing and OTP enumeration that bypasses HTTP-level rate limiting.
- Schema exposure — unrestricted introspection reveals the full data model, accelerating identification of sensitive fields and relationships for targeted data extraction.
Detection
- Test query depth limits — construct a recursive query to the maximum nesting depth supported by the schema (use introspection to identify circular relationships) and observe server response time and CPU usage.
- Test alias bombs — send a query with 100-1000 aliases for the most computationally expensive field and measure response time and resource usage.
- Test batch limits — send a JSON array of 100 identical queries in a single request and verify that the server imposes a per-batch limit.
- Check introspection availability — send
{ __schema { types { name } } }to the API. In production, this should return an error or empty result. - Measure query complexity costs — use a tool like
graphql-query-complexityto assign cost scores to the schema and verify that high-complexity queries are rejected.
Remediation
Implement query depth limiting. Use graphql-depth-limit (Node.js) or equivalent to reject queries exceeding a configured maximum depth:
const depthLimit = require('graphql-depth-limit');
app.use('/graphql', graphqlHTTP({
schema,
validationRules: [depthLimit(5)]
}));
Implement query complexity analysis. Assign cost weights to fields and resolvers. Reject queries whose total computed cost exceeds a configured threshold using graphql-query-complexity.
Disable or restrict batching. Limit batch size to a small number (e.g., 10 operations) and apply per-operation rate limiting inside batch processing logic.
Disable introspection in production. Restrict introspection to authenticated users or disable it entirely in production environments:
app.use('/graphql', graphqlHTTP({ schema, introspection: process.env.NODE_ENV !== 'production' }));
