GraphQL

GraphQL Security

18 min Lesson 27 of 35

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:
  1. Implement depth limiting with a maximum depth of 4 levels
  2. Add rate limiting to your GraphQL endpoint (100 requests per 15 minutes)
  3. Disable introspection and playground in production mode
  4. Create input validation for a user registration mutation
  5. Implement JWT authentication with context-based user verification