Security
GraphQL Security in Code Review: Authorization and Query Complexity
Jun 11, 2025

GraphQL's flexibility comes with unique security challenges that traditional REST API security doesn't cover. Authorization bypasses through query manipulation, resource exhaustion via deep nesting, and N+1 query performance attacks are just the beginning. This comprehensive guide covers the security patterns every code reviewer needs to understand when reviewing GraphQL implementations.
Key Takeaways
•Authorization at the resolver level: GraphQL's granular data fetching requires field-level authorization checks, not just endpoint-level authentication.
•Implement query complexity limits: Use depth limiting, query cost analysis, and rate limiting to prevent resource exhaustion attacks.
•Monitor query patterns: Automated analysis of GraphQL queries can detect malicious patterns and performance issues before they impact production.
GraphQL Security Model Fundamentals
Unlike REST APIs where you secure endpoints, GraphQL requires a more nuanced approach. Every field in your schema is a potential data access point, and the query language allows clients to craft complex requests that can bypass traditional security measures.
The Authorization Challenge
❌ Common Security Gaps
- • Endpoint-only authentication
- • Missing field-level authorization
- • Overly permissive schemas
- • Exposed internal fields
- • No query depth limiting
- • Verbose error messages
- • Missing introspection controls
- • Unvalidated query complexity
✅ Secure GraphQL Patterns
- • Field-level authorization
- • Query depth analysis
- • Resource cost calculation
- • Query whitelisting
- • Introspection disabled in production
- • Sanitized error responses
- • Rate limiting by complexity
- • Resolver-level security checks
⚠️ GraphQL Security Mindset
Think schema-first security. In GraphQL, every field is an API endpoint. Design your authorization model to work at the resolver level, not just the query level.
Authorization Patterns and Anti-Patterns
Authorization in GraphQL requires careful consideration of how data relationships work. A user might have access to a resource directly but not through certain graph traversal paths.
Field-Level Authorization
🚫 Authorization Bypass Vulnerability
// VULNERABLE - No field-level authorization
const resolvers = {
Query: {
user: async (parent, { id }, { user }) => {
if (!user) throw new Error('Unauthorized');
return getUserById(id); // Any authenticated user can access any user
}
},
User: {
email: (parent) => parent.email, // Exposed to all authenticated users
socialSecurityNumber: (parent) => parent.ssn // CRITICAL: No authorization check
}
};
✅ Secure Field-Level Authorization
// SECURE - Proper field-level authorization
const resolvers = {
Query: {
user: async (parent, { id }, { user }) => {
if (!user) throw new Error('Unauthorized');
if (user.id !== id && !user.isAdmin) {
throw new Error('Forbidden');
}
return getUserById(id);
}
},
User: {
email: (parent, args, { user }) => {
if (user.id !== parent.id && !user.isAdmin) {
return null; // Hide field instead of throwing error
}
return parent.email;
},
socialSecurityNumber: (parent, args, { user }) => {
if (!user.isAdmin) {
return null; // Only admins can access PII
}
return parent.ssn;
}
}
};
Authorization Directive Pattern
Using GraphQL directives for authorization provides a cleaner, more maintainable approach to securing your schema.
# Schema with authorization directives
type User {
id: ID!
username: String!
email: String! @auth(requires: USER) @owner
role: String! @auth(requires: ADMIN)
socialSecurityNumber: String @auth(requires: ADMIN)
posts: [Post!]! @auth(requires: USER)
}
type Post {
id: ID!
title: String!
content: String! @auth(requires: USER) @owner
author: User!
}
directive @auth(
requires: Role = USER
) on OBJECT | FIELD_DEFINITION
directive @owner on FIELD_DEFINITION
enum Role {
USER
ADMIN
}
Query Complexity and Resource Protection
GraphQL's nested query capability can be exploited to create resource exhaustion attacks. Implementing proper query analysis prevents these attacks while maintaining API flexibility.
Depth Limiting
⚠️ Deep Query Attack Vector
query DeepQuery {
user(id: "1") {
posts {
author {
posts {
author {
posts {
author {
# This could go 50+ levels deep
# causing exponential resource usage
posts {
title
}
}
}
}
}
}
}
}
}
✅ Query Depth Limiting Implementation
import { depthLimit } from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-query-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7), // Maximum query depth
createComplexityLimitRule(1000, {
maximumComplexity: 1000,
variables: {},
introspection: false,
scalarCost: 1,
objectCost: 1,
listFactor: 10,
createError: (max, actual) => {
return new Error(
`Query complexity ${actual} exceeds maximum allowed complexity ${max}`
);
}
})
]
});
Query Cost Analysis
| Query Element | Base Cost | Multiplier Factor | Max Recommended |
|---|---|---|---|
| Scalar Fields | 1 point | 1x | Unlimited |
| Object Fields | 2 points | 1x | 500 |
| List Fields | 2 points | 10x | 50 |
| Database Queries | 5 points | 1x | 20 |
N+1 Query Problem Detection
The N+1 query problem is particularly dangerous in GraphQL because nested queries can trigger exponential database calls. Code reviewers must understand dataloader patterns and resolver efficiency.
Identifying N+1 Patterns in Code Review
🚫 N+1 Query Anti-Pattern
// PROBLEMATIC - Causes N+1 queries
const resolvers = {
Query: {
posts: () => getAllPosts() // Returns 100 posts
},
Post: {
author: (post) => getUserById(post.authorId), // Called 100 times!
comments: (post) => getCommentsByPostId(post.id), // Called 100 times!
tags: (post) => getTagsByPostId(post.id) // Called 100 times!
}
};
// This query would trigger 1 + 100 + 100 + 100 = 301 database queries
// query {
// posts {
// title
// author { name }
// comments { content }
// tags { name }
// }
// }
✅ DataLoader Solution
import DataLoader from 'dataloader';
// EFFICIENT - Uses batching and caching
const createLoaders = () => ({
userLoader: new DataLoader(async (userIds) => {
const users = await getUsersByIds(userIds);
return userIds.map(id => users.find(user => user.id === id));
}),
commentLoader: new DataLoader(async (postIds) => {
const comments = await getCommentsByPostIds(postIds);
return postIds.map(id => comments.filter(comment => comment.postId === id));
}),
tagLoader: new DataLoader(async (postIds) => {
const tags = await getTagsByPostIds(postIds);
return postIds.map(id => tags.filter(tag => tag.postId === id));
})
});
const resolvers = {
Query: {
posts: () => getAllPosts()
},
Post: {
author: (post, args, { loaders }) => loaders.userLoader.load(post.authorId),
comments: (post, args, { loaders }) => loaders.commentLoader.load(post.id),
tags: (post, args, { loaders }) => loaders.tagLoader.load(post.id)
}
};
// Now the same query triggers only 4 database queries total!
Performance Monitoring Patterns
📊 Query Performance Metrics
- • Resolver execution time per field
- • Database query count and duration
- • Memory usage during query execution
- • Cache hit/miss ratios for DataLoaders
🚨 Alert Thresholds
- • Query execution time > 5 seconds
- • Database queries per request > 20
- • Query complexity score > 1000
- • Memory usage > 100MB per query
Information Disclosure Prevention
GraphQL's introspection feature and verbose error messages can leak sensitive information about your system architecture and data structure.
Introspection Security
⚠️ Production Risk
query IntrospectionQuery {
__schema {
types {
name
fields {
name
type {
name
}
}
}
}
}
This query reveals your entire schema structure to attackers.
✅ Secure Configuration
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: false,
playground: false,
formatError: (error) => {
console.error(error);
return new Error('Internal server error');
}
});
Disable introspection and sanitize errors in production.
Error Handling Security
// Secure error handling with different environments
const formatError = (error) => {
// Log full error details for debugging
console.error('GraphQL Error:', {
message: error.message,
locations: error.locations,
path: error.path,
stack: error.stack
});
// Return sanitized error based on environment
if (process.env.NODE_ENV === 'production') {
// Generic error message in production
if (error.message.includes('unauthorized')) {
return new Error('Authentication required');
}
if (error.message.includes('forbidden')) {
return new Error('Access denied');
}
return new Error('Something went wrong');
}
// Detailed errors in development
return error;
};
Automated GraphQL Security Testing
Manual code review catches many issues, but automated testing is essential for comprehensive GraphQL security coverage.
Security Testing Tools
GraphQL Cop
Security auditing for GraphQL endpoints
• Introspection detection
• Query complexity analysis
• Common vulnerability scanning
• Automated testing
InQL
GraphQL security scanner
• Schema analysis
• Mutation testing
• Authorization bypass detection
• Burp Suite integration
GraphQL Voyager
Schema visualization and analysis
• Schema structure mapping
• Relationship analysis
• Security hotspot identification
• Interactive exploration
GraphQL Security Review Checklist
🔍 Complete Security Review Checklist
Authorization: Field-level access controls implemented, not just query-level
Query Complexity: Depth limiting and cost analysis configured
N+1 Prevention: DataLoaders or equivalent batching implemented
Rate Limiting: Query complexity-based rate limiting enabled
Error Handling: Production errors sanitized, no stack traces exposed
Introspection: Disabled in production environments
Query Validation: Input validation and sanitization implemented
Monitoring: Query performance and security metrics trackedAdvanced Security Patterns
Beyond basic security measures, advanced GraphQL applications require sophisticated patterns to handle complex authorization scenarios and performance optimizations.
Query Whitelisting
🔒 Production Query Control
Query whitelisting allows only pre-approved queries to execute in production, providing the highest level of security but reducing flexibility.
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
require('graphql-query-whitelist')({
whitelist: allowedQueries,
throwError: true
})
]
});
Resource-Based Authorization
🏗️ RBAC Implementation
- • Role-based field access
- • Dynamic permission evaluation
- • Context-aware authorization
- • Resource ownership checks
🎯 ABAC Patterns
- • Attribute-based decisions
- • Policy engine integration
- • Multi-factor authorization
- • Time-based access controls
🔐 GraphQL security requires a schema-first, defense-in-depth approach.


