GraphQL Security Best Practices
While GraphQL's flexibility is powerful, it also brings unique security challenges. Here are key security measures to protect GraphQL APIs.
1. Authentication and Authorization
Authentication Mechanism
javascriptconst { ApolloServer } = require('apollo-server'); const jwt = require('jsonwebtoken'); const server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => { // Get token from request header const token = req.headers.authorization || ''; try { // Verify token const decoded = jwt.verify(token, process.env.JWT_SECRET); return { user: decoded }; } catch (error) { return { user: null }; } } });
Authorization Middleware
javascriptconst { AuthenticationError, ForbiddenError } = require('apollo-server-express'); const resolvers = { Query: { me: (parent, args, context) => { if (!context.user) { throw new AuthenticationError('Not authenticated'); } return context.user; }, users: (parent, args, context) => { if (!context.user || context.user.role !== 'ADMIN') { throw new ForbiddenError('Admin permission required'); } return User.findAll(); } } };
2. Query Depth Limiting
Prevent Deep Nesting Attacks
javascriptconst depthLimit = require('graphql-depth-limit'); const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ depthLimit(7, { ignore: ['ignoredField'] // Ignore specific fields }) ] });
Why needed:
- Prevent malicious clients from sending deeply nested queries
- Avoid server resource exhaustion
- Prevent DoS attacks
3. Query Complexity Limiting
Limit Query Complexity
javascriptconst { createComplexityLimitRule } = require('graphql-validation-complexity'); const complexityLimitRule = createComplexityLimitRule(1000, { onCost: (cost) => console.log(`Query complexity: ${cost}`), createError: (max, actual) => { return new Error(`Query complexity ${actual} exceeds maximum limit ${max}`); } }); const server = new ApolloServer({ typeDefs, resolvers, validationRules: [complexityLimitRule] });
Custom Complexity Calculation
javascriptconst typeDefs = ` type Query { user(id: ID!): User @cost(complexity: 1) users(limit: Int!): [User!]! @cost(complexity: 5, multipliers: ["limit"]) posts(first: Int!): [Post!]! @cost(complexity: 10, multipliers: ["first"]) } `;
4. Rate Limiting
Using express-rate-limit
javascriptconst rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests message: 'Too many requests, please try again later' }); app.use('/graphql', limiter);
GraphQL-specific Rate Limiting
javascriptconst { GraphQLComplexityLimit } = require('graphql-complexity-limit'); const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ new GraphQLComplexityLimit({ maxComplexity: 1000, onExceed: (complexity) => { throw new Error(`Query complexity ${complexity} exceeds limit`); } }) ] });
5. Input Validation
Using Yup Validation
javascriptconst yup = require('yup'); const createUserSchema = yup.object().shape({ name: yup.string().required().min(2).max(100), email: yup.string().email().required(), age: yup.number().min(0).max(150).optional() }); const resolvers = { Mutation: { createUser: async (_, { input }) => { // Validate input await createUserSchema.validate(input); // Create user return User.create(input); } } };
GraphQL Schema Validation
graphqlinput CreateUserInput { name: String! @constraint(minLength: 2, maxLength: 100) email: String! @constraint(format: "email") age: Int @constraint(min: 0, max: 150) }
6. Field-level Permission Control
Using Directives
graphqldirective @auth(requires: Role) on FIELD_DEFINITION enum Role { USER ADMIN } type User { id: ID! name: String! email: String! @auth(requires: ADMIN) salary: Float @auth(requires: ADMIN) }
javascriptconst resolvers = { User: { email: (user, args, context) => { if (context.user?.role !== 'ADMIN') { throw new ForbiddenError('No permission to access this field'); } return user.email; }, salary: (user, args, context) => { if (context.user?.role !== 'ADMIN') { throw new ForbiddenError('No permission to access this field'); } return user.salary; } } };
7. Prevent Query Injection
Parameterized Queries
javascript// Bad practice - string concatenation const query = `SELECT * FROM users WHERE id = '${userId}'`; // Good practice - parameterized queries const query = 'SELECT * FROM users WHERE id = ?'; const result = await db.query(query, [userId]);
Using ORM
javascript// Using Sequelize ORM const user = await User.findOne({ where: { id: userId } });
8. CORS Configuration
javascriptconst server = new ApolloServer({ typeDefs, resolvers, cors: { origin: ['https://yourdomain.com'], // Only allow specific domains credentials: true, methods: ['POST'] } });
9. Error Handling
Don't Expose Sensitive Information
javascriptconst server = new ApolloServer({ typeDefs, resolvers, formatError: (error) => { // Don't expose detailed error information in production if (process.env.NODE_ENV === 'production') { return new Error('Internal server error'); } // Return full error information in development return error; } });
Custom Error Types
graphqltype Error { code: String! message: String! field: String } type UserResult { user: User errors: [Error!]! } type Mutation { createUser(input: CreateUserInput!): UserResult! }
10. Logging and Monitoring
Log Query Logs
javascriptconst server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ didResolveOperation: (context) => { console.log('Query:', context.request.operationName); console.log('Variables:', JSON.stringify(context.request.variables)); } }) } ] });
Monitor Anomalous Queries
javascriptconst server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ didEncounterErrors: (context) => { context.errors.forEach(error => { console.error('Query error:', error.message); // Send to monitoring system monitoringService.logError(error); }); } }) } ] });
11. Query Whitelist
Using Persisted Queries
javascriptconst { createPersistedQueryLink } = require('@apollo/client/link/persisted-queries'); const { sha256 } = require('crypto-hash'); const link = createPersistedQueryLink({ sha256, useGETForHashedQueries: true }); // Only allow predefined queries const allowedQueries = new Set([ 'hash1', 'hash2' ]); const server = new ApolloServer({ typeDefs, resolvers, persistedQueries: { cache: new Map(), ttl: 3600 } });
12. Security Checklist
- Implement authentication mechanism (JWT, OAuth)
- Implement authorization mechanism (role-based access control)
- Limit query depth
- Limit query complexity
- Implement rate limiting
- Validate all inputs
- Implement field-level permission control
- Prevent query injection
- Configure CORS
- Handle errors properly
- Log query logs
- Monitor anomalous queries
- Use query whitelist
- Regular security audits
- Keep dependencies updated
13. Common Security Threats and Protections
| Threat | Description | Protection |
|---|---|---|
| Deep nesting attack | Malicious clients send deeply nested queries | Limit query depth |
| Complexity attack | Send high-complexity queries to consume resources | Limit query complexity |
| DoS attack | Large number of requests make service unavailable | Rate limiting, query whitelist |
| Injection attack | Malicious input causes SQL injection | Parameterized queries, input validation |
| Unauthorized access | Access unauthorized data | Authentication, authorization, field-level permission control |
| Information leakage | Error messages expose sensitive data | Proper error handling |