Security is paramount when building GraphQL APIs. Let's explore advanced security patterns and implementations to protect your GraphQL endpoints from common vulnerabilities and attacks.
Understanding GraphQL Security Challenges 🛡️
GraphQL's flexibility can introduce unique security challenges:
- Query complexity and nested queries
- Resource exhaustion
- Authentication and authorization
- Information disclosure
- Injection attacks
Query Complexity Analysis
Implement query complexity calculation:
const complexityLimit = 1000;
const calculateComplexity = (ast) => {
let complexity = 0;
const visitor = {
Field: {
enter(node) {
// Base cost for each field
complexity += 1;
// Additional cost for nested queries
if (node.selectionSet) {
complexity += 2;
}
}
}
};
visit(ast, visitor);
return complexity;
};
Implement the complexity validator:
const complexityValidator = (schema, document, maxComplexity) => {
const complexity = calculateComplexity(document);
if (complexity > maxComplexity) {
throw new Error(
`Query is too complex: ${complexity}. ` +
`Maximum allowed complexity: ${maxComplexity}`
);
}
};
Rate Limiting Implementation
Create a rate limiter using Redis:
import Redis from 'ioredis';
const redis = new Redis();
const rateLimiter = async (userId, limit, window) => {
const key = `rate_limit:${userId}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, window);
}
if (current > limit) {
throw new Error('Rate limit exceeded');
}
return current;
};
Apply rate limiting to your GraphQL server:
const server = new ApolloServer({
schema,
context: async ({ req }) => {
const userId = getUserFromToken(req.headers.authorization);
await rateLimiter(userId, 100, 3600); // 100 requests per hour
return { userId };
}
});
Query Depth Limiting
Implement depth limiting middleware:
const MAX_DEPTH = 5;
const calculateDepth = (node, depth = 0) => {
if (!node.selectionSet) return depth;
return Math.max(
...node.selectionSet.selections.map(selection =>
calculateDepth(selection, depth + 1)
)
);
};
const depthLimitMiddleware = (resolve, root, args, context, info) => {
const depth = calculateDepth(info.operation);
if (depth > MAX_DEPTH) {
throw new Error(
`Query depth of ${depth} exceeds maximum depth of ${MAX_DEPTH}`
);
}
return resolve(root, args, context, info);
};
Field-Level Security
Implement field-level authorization:
const typeDefs = gql`
directive @auth(
requires: Role = USER
) on FIELD_DEFINITION
enum Role {
ADMIN
USER
GUEST
}
type User {
id: ID!
email: String! @auth(requires: ADMIN)
name: String!
role: Role!
}
`;
const resolvers = {
User: {
email: (parent, args, context) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
return parent.email;
}
}
};
Input Sanitization
Implement input sanitization:
const sanitizeInput = (args) => {
const sanitized = {};
Object.entries(args).forEach(([key, value]) => {
if (typeof value === 'string') {
sanitized[key] = DOMPurify.sanitize(value);
} else if (Array.isArray(value)) {
sanitized[key] = value.map(item =>
typeof item === 'string' ? DOMPurify.sanitize(item) : item
);
} else {
sanitized[key] = value;
}
});
return sanitized;
};
Persisted Queries
Implement persisted queries for production:
const persistedQueries = {
'abc123': 'query GetUser($id: ID!) { user(id: $id) { id name } }'
};
const validatePersistedQuery = (hash) => {
if (!persistedQueries[hash]) {
throw new Error('Invalid persisted query hash');
}
return persistedQueries[hash];
};
Error Handling and Masking
Implement error masking:
const formatError = (error) => {
console.error('GraphQL Error:', error);
if (error.originalError instanceof AuthenticationError) {
return new Error('Authentication required');
}
if (error.originalError instanceof ForbiddenError) {
return new Error('Not authorized');
}
// Generic error for production
return new Error('An error occurred');
};
Introspection Control
Disable introspection in production:
const schema = makeExecutableSchema({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production'
});
Security Headers Implementation
Configure security headers:
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'"
);
next();
});
Monitoring and Logging
Implement security monitoring:
const securityLogger = {
log: (message, metadata) => {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'security',
message,
...metadata
}));
}
};
const monitoringPlugin = {
requestDidStart({ request, context }) {
const startTime = process.hrtime();
return {
willSendResponse({ response }) {
const [seconds, nanoseconds] = process.hrtime(startTime);
const duration = seconds * 1000 + nanoseconds / 1000000;
securityLogger.log('GraphQL Request', {
duration,
success: !response.errors,
userId: context.userId
});
}
};
}
};
Best Practices Summary
- Query Protection
- Implement complexity analysis
- Use depth limiting
- Enable persisted queries in production
- Authentication & Authorization
- Use field-level security
- Implement role-based access control
- Validate JWT tokens properly
- Input Validation
- Sanitize all inputs
- Validate query structure
- Implement custom scalars for specific types
- Production Security
- Disable introspection
- Implement proper error masking
- Use security headers
- Enable rate limiting
Conclusion
Implementing these security patterns will help protect your GraphQL API from common vulnerabilities and attacks. Remember to:
- Regularly audit your security measures
- Keep dependencies updated
- Monitor for suspicious activities
- Test security measures thoroughly
- Document security practices
Stay vigilant and keep your GraphQL implementation secure by regularly reviewing and updating these security patterns as new threats emerge.