GraphQL
GraphQL Security
GraphQL Security
Learn essential security practices to protect your GraphQL API from common vulnerabilities and attacks.
Query Complexity Attacks
GraphQL's flexibility can be exploited with deeply nested or expensive queries:
# Malicious query - fetches massive amounts of data
query MaliciousQuery {
users {
posts {
comments {
author {
posts {
comments {
author {
posts {
comments {
id
}
}
}
}
}
}
}
}
}
}
Warning: Without protection, this query could return millions of records and crash your server. Always implement query complexity analysis.
Depth Limiting
Limit the maximum depth of queries to prevent deeply nested attacks:
const depthLimit = require('graphql-depth-limit');
const { ApolloServer } = require('apollo-server');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)], // Max depth of 5 levels
});
// Custom depth limit with error message
const customDepthLimit = (maxDepth) => {
return (context) => {
return {
Field(node) {
if (node.selectionSet) {
const depth = getDepth(node);
if (depth > maxDepth) {
context.reportError(
new GraphQLError(
`Query depth of ${depth} exceeds maximum allowed depth of ${maxDepth}`
)
);
}
}
},
};
};
};
Rate Limiting
Implement rate limiting to prevent abuse and DDoS attacks:
const { ApolloServer } = require('apollo-server-express');
const rateLimit = require('express-rate-limit');
const express = require('express');
const app = express();
// Global rate limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
});
app.use('/graphql', limiter);
// Per-resolver rate limiting
const rateLimitDirective = {
RateLimit: class extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
const { max, window } = this.args;
field.resolve = async function (...args) {
const [, , context] = args;
const key = `${context.user?.id || context.ip}:${field.name}`;
const count = await redisClient.incr(key);
if (count === 1) {
await redisClient.expire(key, window);
}
if (count > max) {
throw new Error('Rate limit exceeded');
}
return resolve.apply(this, args);
};
},
},
};
// Usage in schema
const typeDefs = gql`
directive @rateLimit(max: Int!, window: Int!) on FIELD_DEFINITION
type Query {
expensiveQuery: Result @rateLimit(max: 10, window: 60)
}
`;
Introspection in Production
Disable introspection queries in production to hide your schema structure:
const { ApolloServer } = require('apollo-server');
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production',
});
// Custom introspection disabling
const { NoIntrospection } = require('graphql-disable-introspection');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
process.env.NODE_ENV === 'production' ? NoIntrospection : null,
].filter(Boolean),
});
Note: Disabling introspection doesn't provide complete security through obscurity, but it raises the bar for attackers and prevents automated schema discovery.
Input Sanitization
Always validate and sanitize user inputs to prevent injection attacks:
const validator = require('validator');
const resolvers = {
Mutation: {
createUser: async (_, { input }, { db }) => {
// Validate email
if (!validator.isEmail(input.email)) {
throw new Error('Invalid email format');
}
// Sanitize strings
const sanitizedName = validator.escape(input.name);
// Validate string length
if (sanitizedName.length < 2 || sanitizedName.length > 50) {
throw new Error('Name must be between 2 and 50 characters');
}
// Check for SQL injection patterns (if using raw SQL)
const sqlInjectionPattern = /(\bOR\b|\bAND\b|;|--|\/\*|\*\/)/i;
if (sqlInjectionPattern.test(input.name)) {
throw new Error('Invalid input detected');
}
// Use parameterized queries
return db.user.create({
data: {
name: sanitizedName,
email: input.email.toLowerCase(),
},
});
},
},
};
CSRF Prevention
Protect against Cross-Site Request Forgery attacks:
const { ApolloServer } = require('apollo-server-express');
const csrf = require('csurf');
const express = require('express');
const app = express();
// Enable CSRF protection
const csrfProtection = csrf({ cookie: true });
// Require CSRF token for mutations
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// Verify CSRF token for mutations
if (req.body.query?.includes('mutation')) {
csrfProtection(req, {}, (err) => {
if (err) throw new Error('Invalid CSRF token');
});
}
return { req };
},
});
// Use CORS properly
const corsOptions = {
origin: ['https://yourdomain.com', 'https://app.yourdomain.com'],
credentials: true,
};
server.applyMiddleware({ app, cors: corsOptions });
Authentication Best Practices
Implement secure authentication and authorization:
const jwt = require('jsonwebtoken');
const { AuthenticationError } = require('apollo-server');
// Verify JWT token
const getUser = (token) => {
try {
if (token) {
return jwt.verify(token, process.env.JWT_SECRET);
}
return null;
} catch (error) {
return null;
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = getUser(token);
return { user };
},
});
// Protected resolver
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) {
throw new AuthenticationError('You must be logged in');
}
return user;
},
},
Mutation: {
deleteUser: async (_, { id }, { user, db }) => {
if (!user) {
throw new AuthenticationError('You must be logged in');
}
if (user.role !== 'ADMIN' && user.id !== id) {
throw new ForbiddenError('You don\'t have permission');
}
return db.user.delete({ where: { id } });
},
},
};
Tip: Use short-lived access tokens (15 minutes) and refresh tokens (7 days) stored in httpOnly cookies for better security.
Query Cost Analysis
Calculate and limit the cost of queries based on complexity:
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const complexityLimit = createComplexityLimitRule(1000, {
// Define field costs
scalarCost: 1,
objectCost: 10,
listFactor: 10,
// Custom field costs
onCost: (cost, { type, field }) => {
if (field.name === 'expensiveField') {
return 100;
}
return cost;
},
});
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [complexityLimit],
});
Exercise:
- Implement depth limiting with a maximum depth of 4 levels
- Add rate limiting to your GraphQL endpoint (100 requests per 15 minutes)
- Disable introspection and playground in production mode
- Create input validation for a user registration mutation
- Implement JWT authentication with context-based user verification