GraphQL's flexibility is powerful—until attackers exploit it. Without proper defenses, queries can expose the entire schema, mutations can bypass validation, and resolvers can leak sensitive data. Securing GraphQL means defending at every layer: schema, query, and resolver.
1. Validate and Limit Query Complexity
Unrestricted queries invite query bombs—attackers craft deeply nested queries that exhaust server resources.
import { buildSchema, graphql } from 'graphql';
import { createComplexityLimitMiddleware } from 'graphql-query-complexity';
const middleware = createComplexityLimitMiddleware({
maximumComplexity: 1000,
variables: {},
onComplete: (complexity) => console.log(`Query complexity: ${complexity}`),
createError: (max, actual) => new Error(`Query too complex: ${actual} > ${max}`)
});
app.use('/graphql', middleware);
2. Authenticate and Authorize Mutations
Every mutation is a write operation. Enforce role-based access and validate permissions before executing.
const resolvers = {
Mutation: {
updateUser: (parent, args, context) => {
if (!context.user) throw new Error('Unauthorized');
if (context.user.id !== args.userId && context.user.role !== 'ADMIN') {
throw new Error('Forbidden: Cannot modify other users');
}
return db.users.update(args.userId, args.data);
}
}
};
3. Hide Sensitive Fields from Schema
Introspection leaks your entire schema. Disable it in production and control field visibility per role.
const apollo = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
context: ({ req }) => ({
user: verifyToken(req.headers.authorization),
isIntrospectionAllowed: process.env.NODE_ENV === 'development'
})
});
4. Sanitize Resolver Arguments
User input in mutations should be treated as hostile until proven otherwise.
import validator from 'validator';
const resolvers = {
Mutation: {
createPost: (parent, args) => {
const sanitized = {
title: validator.escape(args.title),
content: validator.trim(args.content),
tags: args.tags.map(t => validator.escape(t))
};
return db.posts.create(sanitized);
}
}
};
5. Rate Limit Per User
Prevent mutations from being used as attack vectors by enforcing per-user rate limits.
import rateLimit from 'graphql-rate-limit';
const rateLimitDirective = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10
});
const typeDefs = `
type Mutation {
createPost(title: String!): Post @rateLimit(window: "15m", limit: 10)
}
`;
GraphQL's power comes from its precision. That same precision must apply to security.
With proper validation, complexity limits, and role-based access, GraphQL becomes a secure foundation for modern APIs.
Thanks for reading! If this post helped you understand GraphQL security patterns, please share it with your team or leave a comment with your own security wins.
I help teams implement GraphQL architectures that balance flexibility with bulletproof security.
Explore more: kodex.studio
Top comments (0)