GraphQL Security in Code Review: Authorization and Query Complexity

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
Advanced 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.
Secure Your GraphQL APIs
Propel's AI automatically identifies authorization bypasses, query complexity issues, and N+1 problems in your GraphQL code reviews.