GraphQL Security Considerations
GraphQL Security Considerations
GraphQL's flexibility introduces unique security challenges. Query depth attacks can exhaust server resources through deeply nested queries. Query complexity attacks combine multiple expensive operations. Introspection in production environments can expose entire API schemas to attackers. These risks require GraphQL-specific security measures beyond traditional REST API protections.
Query depth limiting prevents nested query attacks by rejecting queries exceeding specified depth. Query complexity analysis assigns costs to different operations and rejects queries exceeding complexity budgets. Field-level authorization ensures users can only access permitted data regardless of query structure. Persistent queries eliminate arbitrary query execution by requiring pre-approved query templates.
// Example: GraphQL security implementation
const depthLimit = require('graphql-depth-limit');
const costAnalysis = require('graphql-cost-analysis');
const { GraphQLError } = require('graphql');
class GraphQLSecurityMiddleware {
constructor(config) {
this.config = config;
this.queryWhitelist = new Map();
this.queryCache = new LRUCache({ max: 1000 });
}
getSecurityRules() {
return [
// Depth limiting
depthLimit(this.config.maxDepth || 7),
// Query cost analysis
costAnalysis({
maximumCost: this.config.maxCost || 1000,
defaultCost: 1,
scalarCost: 1,
objectCost: 2,
listFactor: 10,
introspectionCost: 1000,
enforceIntrospectionCost: true,
createError: (max, actual) => {
return new GraphQLError(
`Query exceeded maximum cost of ${max}. Actual cost: ${actual}`
);
},
onComplete: (cost) => {
// Log expensive queries
if (cost > this.config.maxCost * 0.8) {
this.logExpensiveQuery(cost);
}
}
}),
// Custom security rules
this.createCustomSecurityRules()
];
}
createCustomSecurityRules() {
return {
// Disable introspection in production
validationRules: ({ params, request }) => {
if (this.config.env === 'production') {
return [
require('graphql-disable-introspection').disableIntrospection
];
}
return [];
},
// Query whitelisting for high-security mode
preExecute: async ({ params, request }) => {
if (this.config.whitelistMode) {
const queryHash = this.hashQuery(params.query);
if (!this.queryWhitelist.has(queryHash)) {
throw new GraphQLError('Query not whitelisted');
}
}
// Rate limiting per query type
await this.enforceQueryRateLimit(params, request);
},
// Field-level authorization
fieldResolver: (next) => async (source, args, context, info) => {
// Check field-level permissions
const authorized = await this.checkFieldAuthorization(
info.fieldName,
info.parentType.name,
context.user
);
if (!authorized) {
throw new GraphQLError(
`Unauthorized access to field ${info.parentType.name}.${info.fieldName}`
);
}
// Apply data masking if needed
const result = await next(source, args, context, info);
return this.applyDataMasking(result, info, context);
}
};
}
async checkFieldAuthorization(fieldName, typeName, user) {
// Define field-level permissions
const permissions = {
'User.email': ['self', 'admin'],
'User.ssn': ['self:verified', 'admin:verified'],
'Order.total': ['self', 'admin', 'finance'],
'CreditCard.number': ['payment_processor']
};
const requiredPerms = permissions[`${typeName}.${fieldName}`];
if (!requiredPerms) return true; // No restrictions
// Check user permissions
return requiredPerms.some(perm => {
if (perm.includes(':')) {
const [role, requirement] = perm.split(':');
return user.roles.includes(role) &&
this.meetsRequirement(user, requirement);
}
return user.roles.includes(perm);
});
}
applyDataMasking(data, info, context) {
// Mask sensitive data based on user permissions
const maskingRules = {
'CreditCard.number': (value, user) => {
if (!user.roles.includes('payment_processor')) {
return value.replace(/\d(?=\d{4})/g, '*');
}
return value;
},
'User.ssn': (value, user) => {
if (!user.roles.includes('admin')) {
return '***-**-' + value.slice(-4);
}
return value;
}
};
const rule = maskingRules[`${info.parentType.name}.${info.fieldName}`];
if (rule && data) {
return rule(data, context.user);
}
return data;
}
// Prevent aliasing attacks
validateQueryAliasing(query) {
const ast = parse(query);
const fieldCounts = new Map();
visit(ast, {
Field(node) {
const fieldName = node.name.value;
const count = fieldCounts.get(fieldName) || 0;
fieldCounts.set(fieldName, count + 1);
if (count + 1 > this.config.maxAliasCount) {
throw new GraphQLError(
`Excessive aliasing detected for field: ${fieldName}`
);
}
}
});
}
}