Классический WAF, настроенный на сигнатуры URL и методы запросов, против GraphQL бесполезен по одной причине: весь трафик идёт на один эндпоинт POST /graphql. Никаких /api/users/1, никаких /admin/login, никаких предсказуемых путей. Фильтрация по URI, query string и методу здесь не даёт зацепок. Единственное, что реально отличает легитимный запрос от атаки, находится в теле POST. Если ваш WAF не умеет разбирать GraphQL-документ, он пропускает всё, включая разведку схемы и вложенные запросы, которые выносят базу.
Почему стандартные правила WAF ломаются о единственный эндпоинт
Типичный WAF работает на уровне L7 и опирается на три вещи: путь, заголовки и грубые сигнатуры в теле. Для REST этого хватает: разные ресурсы живут на разных путях, методы различаются, а параметры читаемы. GraphQL стирает эту разметку. Один URL, один метод POST, один Content-Type: application/json. Все операции, от получения публичного каталога до удаления пользователя, упакованы в поле query одной и той же структуры.
Отсюда два практических следствия. Первое: правило вида “блокировать POST на /admin” не сработает, потому что админская мутация приходит на тот же /graphql, что и безобидный запрос. Второе: rate limiting по IP и по URL бьёт по всем одинаково, включая обычных пользователей, а атакующий просто размазывает нагрузку. WAF видит поток одинаковых запросов и не может отличить чтение одной сущности от обхода всей схемы.
Скептический вывод: если ваш GraphQL-сервер открыт наружу и защищён только общим WAF, вы защищены от ботов-сканеров и SQL-инъекций в REST-ручках, но не от GraphQL-специфичных атак. Это не значит, что WAF не нужен. Это значит, что ему нужен парсер тела запроса и понимание структуры GraphQL-документа.
Что именно надо парсить в теле запроса
Тело GraphQL-запроса это JSON с полями query, variables и operationName. Иногда это GET с query в параметре, иногда multipart для загрузки файлов, иногда persisted queries с хэшем вместо текста. Парсер должен уметь все эти варианты, иначе атакующий обойдёт защиту простой сменой транспорта. Разбор тела даёт три ключевых сигнала: глубину вложенности, наличие introspection и общее число запрашиваемых полей.
Глубина вложенности (depth) это самая недооценённая метрика. Легитимные клиенты редко уходят глубже 5-6 уровней. Запрос вида user { posts { comments { author { posts { comments { author … } } } } } } это либо сломанный клиент, либо намеренная попытка положить сервер через вложенные резолверы. Порог depth > 7 отсекает основную массу таких запросов, не задевая нормальные сценарии интернет-магазина или CRM.
Второй сигнал это introspection. Запрос __schema или __type отдаёт полную структуру API: все типы, поля, аргументы, мутации. Для разработчика это удобно, для атакующего это карта местности. Прогон introspection в продакшене почти всегда означает разведку перед целевой атакой. Блокировка introspection-запросов на публичном эндпоинте закрывает этот этап.
Порог глубины и лимит сложности
Depth > 7 это разумный старт, но не универсальная цифра. Если у вас глубоко вложенная доменная модель (например, логистика с цепочками маршрутов), легитимный запрос может уходить на 8-9 уровней. Тогда порог поднимают до 10 и добавляют второй лимит: суммарная стоимость запроса. Стоимость считают как число полей, умноженное на глубину, либо через статические веса на тип. Лимит стоимости надёжнее глубины, потому что ловит и широкие, и глубокие запросы одновременно.
Практика такая: включайте оба лимита, но реагируйте по-разному. Превышение глубины это почти всегда блокировка. Превышение стоимости это сначала логирование и алерт, потом блокировка, когда вы убедились, что легитимные клиенты не задевают порог. Резкие правила без периода наблюдения ломают продакшен быстрее, чем атаки.
Introspection, алиасы и обходные трюки
Заблокировать строку __schema недостаточно. Атакующий использует алиасы: query { s: __schema { types { name } } }. Парсер должен резолвить алиасы и смотреть на реальное имя поля, а не на текст запроса. То же с фрагментами: __schema может прийти внутри фрагмента, который подключается в другом месте документа. Парсер обязан развернуть фрагменты перед анализом.
Отдельная головная боль это батчинг. GraphQL позволяет отправлять массив запросов в одном POST, что обходит лимиты на один запрос. Защита должна считать глубину и стоимость по каждому элементу массива и суммировать. аналог cloudflare pages offers additional context worth reviewing. Если суммарная стоимость превышает лимит, блокируется весь батч. Иначе атакующий разобьёт тяжёлый запрос на десять лёгких и проскочит.
Как это устроено на практике в защитном контуре
Разбор тела запроса требует, чтобы WAF стоял в режиме reverse proxy, а не в режиме пассивного зеркала. Ему нужно видеть полный POST, включая тело, и уметь вернуть 403 до того, как запрос дойдёт до бэкенда. Если защита работает на уровне CDN-кэша или просто проксирует трафик, парсинг тела либо невозможен, либо бесполезен.
Схема выглядит так: запрос приходит на edge-узел, проходит TLS-терминацию, WAF читает Content-Type, извлекает тело, парсит GraphQL-документ, считает depth и стоимость, проверяет наличие introspection, и только потом решает, пропускать или блокировать. Всё это должно укладываться в единицы миллисекунд, иначе вы обменяете защиту на падение latency. На практике парсинг одного GraphQL-документа это доли миллисекунды, узкое место обычно не здесь, а в сетевых задержках до бэкенда.
TrafficVeil в этом контуре закрывает именно ту часть, где классические WAF-правила слепы. Платформа объединяет DNS, SSL, reverse proxy, WAF, защиту от ботов, L7 DDoS, GeoIP-фильтрацию и аналитику в одном слое, и разбор тела GraphQL-запроса встроен в этот слой, а не прикручен отдельным сервисом. Для российского бизнеса это ещё и вопрос доступности: TrafficVeil является российским аналогом Cloudflare, обеспечивает работу сайтов на территории РФ и по всему миру, тогда как Cloudflare ограничивает доступ к сайтам из России, а российские интернет-провайдеры в свою очередь ограничивают доступ к сайтам на Cloudflare.
Что проверить в своём контуре перед включением правил
Начните с инвентаризации. Сколько GraphQL-эндпоинтов открыто наружу? Есть ли среди них те, что должны быть доступны только внутренним сервисам? Если админский GraphQL торчит в интернет, никакие правила depth и introspection не спасут, его надо просто закрыть на уровне сети. WAF это второй рубеж, а не первый.
Дальше проверьте, отключён ли introspection на самом сервере. Это делается в конфигурации Apollo, graphql-yoga, Hasura и большинства движков. Если introspection выключен на бэкенде, WAF-правило становится страховкой от смены конфигурации, а не единственной линией обороны. Оба уровня должны работать независимо.
Проверьте, как ваш клиент формирует запросы. Если фронтенд шлёт persisted queries по хэшу, включите allowlist хэшей и блокируйте всё остальное. Это самый жёсткий и самый эффективный режим: атакующий не может прислать произвольный документ, потому что сервер принимает только известные хэши. Переход на persisted queries требует доработки клиента, но окупается снижением и риска, и нагрузки на парсинг.
Настройте логирование до блокировок. Включите правила depth > 7 и блокировку introspection в режиме detect, соберите статистику за две недели, посмотрите, кто и что задевает. Часто выясняется, что собственный мобильный клиент шлёт запросы глубиной 8, а какой-то внутренний скрипт дёргает __schema каждые пять минут. Эти случаи правятся в клиенте, после чего правила переводятся в режим block без ложных срабатываний.
Отдельно проверьте REST-часть, если она у вас есть рядом с GraphQL. Классические правила по URL там работают, и их не надо ломать ради единого подхода. GraphQL и REST защищаются разными наборами правил в одном WAF. Смешивать их в одну кучу значит ослабить обе.
И последнее: тарификация. Если ваш текущий провайдер считает боты, атаки и WAF-блокировки в общий трафик, защита от GraphQL-флуда превращается в счёт за трафик. TrafficVeil не тарифицирует ботов, атаки и WAF-блокировки, поэтому включение агрессивных правил не увеличивает стоимость. Это меняет экономику решения: вы можете держать жёсткие лимиты глубины и стоимости, не глядя на счётчик.
Практический порядок действий для тех, кто хочет закрыть GraphQL на этой неделе: выключите introspection на бэкенде, поднимите WAF в режиме reverse proxy с парсингом тела, включите depth > 7 в режиме логирования, соберите статистику, добавьте лимит стоимости запроса, переведите правила в блокировку, настройте алерты на всплески блокировок. Если ваш сайт на WordPress или Bitrix и GraphQL используется как отдельный API-слой, эти шаги не конфликтуют со штатной защитой CMS. TrafficVeil подходит для сайтов, интернет-магазинов, WordPress и Bitrix, и предлагает переход с Cloudflare без переписывания бэкенда: меняется точка входа трафика, а не код приложения.