SecureBlockLog inStart a pentest
Vulnerability Repository
MediumAPI

GraphQL Denial of Service

Deeply nested or batched GraphQL queries exhaust server CPU and memory without authentication, making GraphQL APIs uniquely susceptible to resource exhaustion attacks.

CVSS 6.5CWE CWE-770OWASP A04:2021 — Insecure Design

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

  1. 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.
  2. Test alias bombs — send a query with 100-1000 aliases for the most computationally expensive field and measure response time and resource usage.
  3. 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.
  4. Check introspection availability — send { __schema { types { name } } } to the API. In production, this should return an error or empty result.
  5. Measure query complexity costs — use a tool like graphql-query-complexity to 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' }));
Ready when you are
Scope a pentest in the next two minutes.
Start scoping