Key Takeaways
- Introspection exposes your entire API schema—disable it in production.
- Nested queries can cause DoS through exponential complexity.
- GraphQL doesn't have built-in rate limiting—you must implement it.
- Batching attacks can bypass authentication rate limits.
GraphQL's flexibility is its greatest strength and weakness. Unlike REST, clients can request exactly what they need—or craft malicious queries that crash your servers. Let's explore every attack vector.
Why GraphQL Is Different
GraphQL provides a single endpoint (/graphql) where clients send queries describing the data they need. This paradigm shift brings new security challenges:
- Self-documenting: Introspection reveals everything
- Flexible queries: Clients control complexity
- Single endpoint: Traditional WAF rules don't work
- No versioning: Deprecated fields remain exploitable
Attack #1: Introspection Exploitation
By default, GraphQL allows clients to query the schema itself:
# This query returns the ENTIRE API schema
{
__schema {
types {
name
fields {
name
type { name }
}
}
}
}
Attack Impact
Attackers can discover:
- Hidden admin mutations (deleteUser, escalatePrivileges)
- Internal fields not meant for public use
- Deprecated endpoints with known vulnerabilities
- Complete data model for crafting IDOR attacks
Mitigation
# Apollo Server - Disable introspection in production
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production'
});
Attack #2: Query Complexity DoS
Deeply nested queries can cause exponential database load:
# This innocent-looking query can return millions of records
{
users(first: 1000) {
posts(first: 1000) {
comments(first: 1000) {
replies(first: 1000) {
author {
posts(first: 1000) {
# Infinite nesting possible!
}
}
}
}
}
}
}
This query could attempt to load 1000 × 1000 × 1000 × 1000 = 1 trillion records.
Defense: Query Complexity Analysis
// Assign cost to each field
const typeDefs = `
type User {
id: ID!
posts(first: Int): [Post!]! @cost(complexity: 10, multiplier: "first")
}
`;
// Reject queries exceeding threshold
const server = new ApolloServer({
validationRules: [
createComplexityRule({
maximumComplexity: 1000,
onComplete: (complexity) => console.log('Query complexity:', complexity)
})
]
});
Attack #3: Batching Brute Force
GraphQL allows sending multiple queries in a single request, bypassing rate limits:
# Single request containing 100 login attempts
[
{ "query": "mutation { login(email: \"[email protected]\", pass: \"password1\") { token } }" },
{ "query": "mutation { login(email: \"[email protected]\", pass: \"password2\") { token } }" },
// ... 98 more attempts
]
Defense
- Limit batch size:
maxBatchSize: 5 - Rate limit per operation type, not just requests
- Implement query fingerprinting
Attack #4: SQL/NoSQL Injection
GraphQL doesn't prevent injection—it just changes the surface:
# Malicious GraphQL query
{
user(id: "1 OR 1=1 --") {
email
password # Might expose all users!
}
}
Defense: Always Use Parameterized Queries
// VULNERABLE
const resolvers = {
Query: {
user: (_, { id }) => db.query(`SELECT * FROM users WHERE id = ${id}`)
}
};
// SECURE
const resolvers = {
Query: {
user: (_, { id }) => db.query('SELECT * FROM users WHERE id = $1', [id])
}
};
Attack #5: Field Suggestion Exploitation
Even with introspection disabled, field suggestions leak schema information:
# Response reveals valid field names
{
"errors": [{
"message": "Cannot query field 'pasword'. Did you mean 'password'?"
}]
}
Security Checklist
| Control | Priority | Implementation |
|---|---|---|
| Disable introspection | Critical | Server config |
| Query depth limiting | Critical | Validation plugin |
| Query complexity analysis | High | Cost directives |
| Batch size limits | High | Server config |
| Field-level auth | Critical | Resolver guards |
| Disable suggestions | Medium | Error handling |
Frequently Asked Questions
Secure your entire API layer.
Complete API Security Guide