Protecting GraphQL from Deeply Nested DoS Attacks
GraphQL's flexible querying capabilities allow clients to request precisely the data they need, but this flexibility makes backends vulnerable to denial-of-service (DoS) attacks through deeply nested queries. Because GraphQL schemas often contain cyclic or recursive relationships—such as a user having friends who also have friends—a malicious client can craft an exponentially complex query that consumes excessive server and database resources. To prevent these vulnerabilities, web developers implement mechanisms such as query depth limiting, query cost analysis, pagination bounds, execution timeouts, and persisted queries.
Maximum Query Depth Limiting
Query depth limiting is the most direct defense against deeply nested
attacks. Before executing an incoming operation, the GraphQL server
analyzes the query’s Abstract Syntax Tree (AST) to determine its maximum
depth. Each nested selection set increments the depth counter. If the
query's depth exceeds a predefined threshold (for instance, a depth
limit of 5 to 7 levels), the server rejects the request immediately
without running any database resolvers. Libraries such as
graphql-depth-limit for Node.js or native depth-validation
rules in engines like Apollo Server make this mechanism straightforward
to implement.
Query Cost Analysis and Complexity Calculation
While depth limiting prevents deep trees, it does not prevent "wide"
attacks where a query asks for dozens of expensive root or secondary
fields. Query cost analysis solves this by assigning a numerical score
or weight to each field based on its computation or retrieval cost.
Resolvers that perform database joins, external API calls, or return
large lists receive higher cost values. The server parses the AST,
calculates the cumulative cost (often taking arguments like
first or limit into account as multipliers),
and rejects any query exceeding a maximum complexity budget.
Enforcing Pagination and List Limits
Nested attacks often rely on unbounded array fields to multiply the
server's work at each level. If a schema allows retrieving a list of
entities without an upper bound, a client can combine moderate nesting
with large batches to cause memory exhaustion. Protecting against this
requires mandating pagination arguments (first,
last, or limit) on all list fields and
strictly enforcing a maximum permissible page size at the schema or
validation level.
Execution and Resolver Timeouts
Even with static AST analysis, certain queries may cause database stalls or unpredicted resource spikes. Setting strict execution timeouts at both the application server level and the database level ensures that runaway queries are forcefully terminated. In GraphQL, this can be implemented through context cancellation, resolver middleware that monitors execution duration, or database query timeout directives.
Persisted Queries and Whitelisting
For applications where the GraphQL backend serves known first-party clients (such as mobile apps or internal web frontends), persisted queries provide the strongest protection against arbitrary query attacks. During the build process, the client compiles its approved queries and generates unique cryptographic hashes for each. In production, the client sends only the hash, and the server executes the corresponding pre-vetted query. The server is configured to reject any arbitrary, unhashed GraphQL queries from the public, entirely eliminating the risk of deeply nested DoS attacks.