How recursive circular schemas, unmetered query complexity, and batching allow attackers to crash GraphQL backends with single HTTP requests.
In traditional REST APIs, if you ask for a user, you get a user. In GraphQL, the client decides what data to ask for. If your database allows users to have friends, and friends to have friends, an attacker can submit a nested query 500 layers deep: `user { friends { friends { friends... } } }`. The server attempts to join millions of database rows, exhausting all CPU and crashing the API for everyone.
An attacker queries { __schema { types { name fields { name } } } } to map the entire data graph.
The attacker identifies self-referencing relationship fields (e.g., Thread -> comments -> replies -> author -> threads).
A 1KB JSON payload is submitted containing 25 levels of nested circular queries.
The server creates thousands of nested database resolver promises, running out of memory (OOM) or tying up all database connections.
# VULNERABLE: Unrestricted GraphQL Query Execution
# An attacker submits this 1KB payload that causes millions of DB joins:
"""
query CircularDoS {
user(id: "1") {
friends {
friends {
friends {
friends {
friends {
friends { name }
}
}
}
}
}
}
}
"""
# VULNERABLE APOLLO SERVER CONFIG
const { ApolloServer } = require("@apollo/server");
const server = new ApolloServer({
typeDefs,
resolvers,
// CRITICAL: No depth validation, no complexity cost limits, introspection enabled!
introspection: true
});
// SECURE: Enforcing Query Depth Limiting & Cost Analysis
const { ApolloServer } = require("@apollo/server");
const depthLimit = require("graphql-depth-limit");
const { createComplexityLimitRule } = require("graphql-validation-complexity");
const server = new ApolloServer({
typeDefs,
resolvers,
// 1. Disable introspection in production environments
introspection: process.env.NODE_ENV !== "production",
// 2. Add validation rules executed BEFORE query runs
validationRules: [
// Reject any query deeper than 5 nested levels
depthLimit(5),
// Assign costs to fields (e.g. lists cost 10x) and reject if total > 1000
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 2,
listFactor: 10,
onCost: (cost) => console.log(`Evaluated query complexity: ${cost}`)
})
]
});
introspection: false) in production.graphql-depth-limit) capped to a safe depth (e.g. 5–7 levels).