Rate Limiting и Depth Limiting для GraphQL API
GraphQL позволяет клиентам формировать произвольно сложные запросы. Без ограничений один запрос может запросить миллионы записей через вложенные связи или создать CPU-killer запрос с глубиной вложенности 50 уровней. Rate limiting для GraphQL отличается от REST: нельзя просто считать запросы — нужно учитывать сложность.
Depth Limiting
Ограничение максимальной глубины вложенности AST-дерева запроса:
import depthLimit from 'graphql-depth-limit'
import { ApolloServer } from '@apollo/server'
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7) // максимум 7 уровней вложенности
]
})
Атака без depth limit:
# Этот запрос легален, но рекурсивно создаёт миллионы объектов
{
user {
friends {
friends {
friends {
friends {
friends { id name }
}
}
}
}
}
}
Query Complexity (сложность запроса)
Depth не учитывает широту запроса. graphql-query-complexity считает суммарную стоимость:
import { createComplexityLimitRule } from 'graphql-query-complexity'
import { fieldExtensionsEstimator, simpleEstimator } from 'graphql-query-complexity'
const complexityRule = createComplexityLimitRule(1000, {
estimators: [
// Брать complexity из SDL @complexity директивы
fieldExtensionsEstimator(),
// Учитывать аргументы пагинации
({
type, field, args, childComplexity
}) => {
if (args.limit) {
return args.limit * childComplexity
}
if (args.first) {
return args.first * childComplexity
}
return 1 + childComplexity
},
// Базовая стоимость поля = 1
simpleEstimator({ defaultComplexity: 1 })
],
onSuccess: (complexity) => {
console.log(`Query complexity: ${complexity}`)
},
formatErrorMessage: (complexity) =>
`Query too complex (${complexity}). Max allowed: 1000`
})
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7),
complexityRule
]
})
Complexity в SDL с директивами
directive @complexity(value: Int!, multipliers: [String!]) on FIELD_DEFINITION
type Query {
# Простое поле: complexity 1
user(id: ID!): User
# Список: complexity = first * childComplexity
posts(first: Int = 10): [Post!]! @complexity(value: 1, multipliers: ["first"])
# Дорогая операция: complexity 10
searchUsers(query: String!): [User!]! @complexity(value: 10)
}
Rate Limiting по операциям
// Разные лимиты для разных типов операций
class GraphQLRateLimiter {
constructor(redis) {
this.r = redis
}
async checkRequest(userId, operationName, complexity) {
const now = Math.floor(Date.now() / 1000)
const minute = now - (now % 60)
// 1. Лимит по количеству операций (запросов) в минуту
const opsKey = `gql:ops:${userId}:${minute}`
const ops = await this.r.incr(opsKey)
this.r.expire(opsKey, 120)
if (ops > 200) {
throw new GraphQLError('Too many requests', {
extensions: { code: 'RATE_LIMITED', retryAfter: 60 }
})
}
// 2. Лимит по суммарной сложности в минуту
const complexityKey = `gql:complexity:${userId}:${minute}`
const totalComplexity = await this.r.incrby(complexityKey, complexity)
this.r.expire(complexityKey, 120)
if (totalComplexity > 10000) {
throw new GraphQLError('Query complexity budget exceeded', {
extensions: { code: 'COMPLEXITY_LIMITED', retryAfter: 60 }
})
}
// 3. Специальные лимиты для дорогих операций
const expensiveOps = ['SearchUsers', 'ExportData', 'GenerateReport']
if (expensiveOps.includes(operationName)) {
const expKey = `gql:expensive:${userId}:${minute}`
const expCount = await this.r.incr(expKey)
this.r.expire(expKey, 120)
if (expCount > 5) {
throw new GraphQLError(`Too many ${operationName} calls`, {
extensions: { code: 'RATE_LIMITED' }
})
}
}
return { allowed: true, remainingOps: 200 - ops }
}
}
Apollo Server Plugin для rate limiting
const rateLimiter = new GraphQLRateLimiter(redis)
const rateLimitPlugin = {
async requestDidStart() {
return {
async executionDidStart({ request, context }) {
const userId = context.user?.id || context.req.ip
const operationName = request.operationName || 'anonymous'
// Complexity рассчитывается до выполнения
const complexity = request.extensions?.queryComplexity || 1
await rateLimiter.checkRequest(userId, operationName, complexity)
}
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [rateLimitPlugin],
validationRules: [depthLimit(7), complexityRule]
})
Защита от Introspection в продакшен
Introspection раскрывает полную схему — нежелательно в продакшен:
import { NoSchemaIntrospectionCustomRule } from 'graphql'
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
validationRules: process.env.NODE_ENV === 'production'
? [NoSchemaIntrospectionCustomRule]
: []
})
Лимиты aliases и directives
Aliases позволяют запросить одно поле с разными аргументами — DoS вектор:
# 1000 одинаковых запросов через aliases
{
a1: user(id: "1") { name }
a2: user(id: "1") { name }
# ... a1000
}
import { createComplexityLimitRule } from 'graphql-query-complexity'
// graphql-armor — комплексная защита
import { createArmor } from '@escape.tech/graphql-armor'
const armor = createArmor({
maxAliases: { n: 15 }, // максимум 15 aliases
maxDirectives: { n: 50 }, // максимум 50 директив
maxDepth: { n: 7 }, // глубина
maxTokens: { n: 1000 }, // токены в запросе
costLimit: {
maxCost: 5000,
objectCost: 2,
scalarCost: 1,
depthCostFactor: 1.5,
ignoreIntrospection: true
}
})
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [...armor.plugins],
validationRules: [...armor.validationRules]
})
Срок выполнения
Настройка depth limiting, query complexity и rate limiting для GraphQL — 1–2 рабочих дня.







