Если вы до сих пор думаете, что OWASP API Security Top 10 - это просто список для галочки в чек-листе аудита, то вы либо не дежурили в прод в три часа ночи, либо ваши API никто не атакует. Атакуют, поверьте. Просто вы пока не знаете. Но сегодня мы это исправим.
Я покажу, как реально автоматизировать тестирование API так, чтобы находить не только SQLi и XSS (которые давно уже ловят даже школьные сканеры), а настоящие дыры: race conditions, баги бизнес-логики, краш сервера через переполнение стека в JSON-схеме, и прочие прелести, от которых у разработчиков дергается глаз.
В этой статье будет всё: от выбора инструментов, которые реально работают, до интеграции в CI/CD, где каждый твой пул-реквест будет прогоняться через ад фаззинга, и если что-то упало - ты не смержишься, пока не починишь. Мы разберем каждую деталь, каждый костыль, каждый хак. Потому что безопасность API - это не про "запустил Nessus и забыл". Это про инженерию.
Пристегните ремни.
1. Зачем API Fuzzing, если у нас есть DAST?
1.1. DAST: история о том, как мы пихаем квадратное в круглое
Для тех, кто только недавно пришел в индустрию (или всё ещё верит вендорам): DAST (Dynamic Application Security Testing) - это такие сканеры, которые шлют в твой сайт или API пачку подготовленных запросов с известными пейлоадами из базы данных сигнатур. Они ищут типовые уязвимости: SQL-инъекции, XSS, path traversal, ну и там command injection. Звучит неплохо, да? Но есть нюанс.Во-первых, DAST работает по принципу "угадай параметр". Если твой API принимает JSON с полем user.id, а сканер думает, что параметр называется user_id, то он вообще не поймет, куда пихать пейлоад. Во-вторых, DAST не понимает состояния. То есть, чтобы выполнить операцию "перевод денег", надо сначала залогиниться, потом получить баланс, потом создать транзакцию. DAST же шлет запросы вразнобой, не зная, что после POST /transfer нужно иметь валидный токен и существующий счет. В итоге 90% запросов падают с 401 Unauthorized, и сканер радостно сообщает: "Уязвимостей не найдено". Ага, как же.
И это я еще молчу про бизнес-логику. DAST не знает, что в твоем приложении "цена товара" - это поле, которое должно быть неотрицательным, и что если в POST /order передать price: -100, то ты либо получишь ошибку, либо (в некоторых системах) - начислишь клиенту деньги. Такой кейс классический сканер не проверит.
А теперь представь, что у тебя микросервисная архитектура с 50+ API, каждый со своей OpenAPI-спецификацией (ну, или без нее, что бывает чаще). Ты запускаешь DAST, он ползает 8 часов, выдает 20 ложных срабатываний и ни одной реальной дыры. Разработчики смеются, безопасники пьют валерьянку, а злоумышленники в это время находят race condition в эндпоинте сброса пароля.
Мораль: DAST хорош для базовой гигиены, но как основной инструмент для API он сосёт. Нам нужно что-то, что будет думать, подстраиваться под схему API, учитывать зависимости между запросами и генерировать нестандартные, иногда безумные, входные данные. И это называется API Fuzzing.
1.2. Что реально находит фаззинг? (спойлер: всё, что вы боялись)
Фаззинг - это метод, при котором мы автоматически генерируем огромное количество (сотни тысяч) запросов с различными входными данными, включая невалидные, граничные, непредсказуемые, и смотрим, как система реагирует. Если API ведет себя не так, как ожидалось (падает с 500, возвращает чувствительные данные, зависает, или выполняет действие, которое не должно было выполниться), мы нашли проблему.Конкретные примеры из жизни:
- 500 Internal Server Error из-за непредвиденного типа данных: передал в поле age строку "восемнадцать", а в коде ожидали число. Разработчики забыли валидацию, и парсер JSON упал с необработанным исключением. В итоге - раскрытие стека трейса с путями к исходникам и версиями библиотек.
- Race condition в процессе регистрации: при одновременной отправке двух запросов на регистрацию с одинаковым email, оба проходили валидацию до того, как первая запись успевала вставиться в БД. Результат - два аккаунта на один email. Дальше можно было разводить акции "приведи друга".
- Обход авторизации через изменение типа данных в токене: API ожидал JWT, а мы в Authorization передали пустой объект {}. В некоторых фреймворках это приводило к тому, что middleware падал, и запрос проходил дальше без проверки. Фаззинг это нашел на 5000-м запросе, когда мы начали мутировать заголовки.
- Data leak через verbose error: при передаче слишком длинного параметра в GET /search сервер возвращал ошибку с полным SQL-запросом, включая данные из других таблиц.
- DoS через рекурсивные сущности в JSON: отправил JSON, где одно поле ссылалось на другое, образуя цикл. Сериализатор ушел в бесконечную рекурсию, и CPU на поде подскочил до 100%.
- Business logic flaw: в API интернет-магазина поле quantity принимало отрицательные значения. Фаззинг нашел, что при quantity: -5 корзина обновлялась, и сумма заказа уменьшалась. Можно было купить товар с отрицательной ценой.
И вот тут возникает ключевое различие: API Fuzzing бывает разным по подходу.
1.3. Три кита API Fuzzing: schema-based, mutation-based, generation-based
Прежде чем кидаться в инструменты, давайте разберемся, какие вообще бывают стратегии фаззинга. Потому что от выбора подхода зависит, что вы найдете, а что пропустите.Schema-based (на основе спецификации)
Это когда у вас есть OpenAPI (Swagger) или GraphQL-схема, и вы генерируете тесты прямо из нее. Инструмент парсит схему, видит типы данных, обязательные поля, enum'ы, и на основе этого строит запросы. Например, если поле id имеет тип integer, фаззер сгенерирует не только валидные числа, но и 0, -1, 2147483648 (overflow), "1" (строку), null и т.д. Если поле status имеет enum ['active', 'inactive'], он подставит 'deleted' или пустую строку.Плюсы: быстро, покрывает все эндпоинты, учитывает структуру. Минусы: если схема неполная или устаревшая (а такое бывает в 99% проектов), вы упустите многое. К тому же, schema-based фаззинг не проверяет бизнес-логику, которая не отражена в схеме (например, что после создания объекта он должен быть доступен только владельцу).
Mutation-based (на основе мутации)
Тут мы берем реальные запросы (например, из логов трафика, коллекций Postman, или HAR-файлов) и мутируем их - меняем значения полей, добавляем новые, удаляем обязательные, меняем типы. Это похоже на то, как работают классические фаззеры вроде AFL, но для API. Мы берем валидный запрос и начинаем его "портить". Например, в POST /user с полями {"name": "John", "age": 30} мы можем попробовать {"name": "John"*10000, "age": 30} (очень длинное имя), {"name": "John", "age": -30}, {"name": "John", "age": "thirty"}, {"name": null, "age": 30}, {"name": {"$ne": null}, "age": 30} (NoSQL-инъекция).Плюсы: не требует схемы, может находить недокументированные эндпоинты и параметры. Минусы: нужно иметь хороший набор "семян" (валидных запросов), иначе мутация будет бессмысленной. Также мутационный фаззинг может не покрыть сложные последовательности действий.
Generation-based (генерационный)
Это когда фаззер генерирует запросы с нуля, используя модель данных, описанную в коде или в схеме, но с добавлением сложных структур. Например, он может генерировать JSON с произвольной вложенностью, случайными ключами, массивами разных размеров. По сути, это более продвинутый вариант schema-based, где вы не просто подставляете значения в предопределенные поля, а можете создавать новые поля, комбинировать их, использовать property-based testing (как в Hypothesis).Генерационный фаззинг особенно хорош для тестирования парсеров и сериализаторов, а также для поиска проблем в сложной логике валидации.
На практике, лучшая стратегия - комбинировать все три. Начинаем со schema-based, чтобы быстро покрыть документированную поверхность. Потом добавляем mutation-based на основе реального трафика, чтобы поймать недокументированные поля. И при необходимости подключаем генерацию сложных структур.
Теперь, когда мы поняли теорию, переходим к самому вкусному - инструментам, которые реально работают, а не просто пылятся в репозиториях.
2. Инструменты: выбираем своего монстра
На рынке куча фаззеров для API, от opensource до enterprise-решений за миллион долларов. Я расскажу про те, которые я сам использовал в бою, которые не требуют миллиарда настроек и реально приносят результат. Это RESTler от Microsoft, Schemathesis (мой любимчик), и APIFuzzer для легких случаев. Плюс отдельно разберем GraphQL-фаззинг, потому что там свои особенности.2.1. RESTler: когда нужно учитывать состояние и зависимости
RESTler - это детище Microsoft Research. Он позиционируется как "stateful REST API fuzzer". Что это значит? Он не просто шлет запросы по одному, а пытается выстроить последовательности, которые имеют смысл с точки зрения приложения. Например, он поймет, что для удаления пользователя нужно сначала создать его, авторизоваться, получить ID, и только потом вызывать DELETE /users/{id}. Для этого RESTler анализирует OpenAPI-спецификацию и выявляет зависимости между эндпоинтами (например, через параметры пути, которые могут быть результатом предыдущего запроса).Установка и сборка
RESTler написан на C# и распространяется в виде исходников. Да, придется собирать. Но не пугайтесь, все делается в пару команд.
Bash:
git clone https://github.com/microsoft/restler-fuzzer.git
cd restler-fuzzer
# Убедитесь, что у вас установлен .NET SDK (версия 6.0 или выше)
dotnet build
После сборки в папке restler-fuzzer/restler появится исполняемый файл Restler.exe (или Restler под Linux, если вы собрали под .NET Core). Я обычно собираю в Docker, чтобы не засорять систему:
Код:
dockerfile
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /app
COPY . .
RUN dotnet build -c Release
RUN dotnet publish -c Release -o out
Но если вы не фанат контейнеров, просто запустите dotnet build и используйте бинарник.
Генерация тестов из OpenAPI спецификации
Первым делом нужно дать RESTler вашу OpenAPI-спецификацию (в формате JSON или YAML). Допустим, она лежит в swagger.json. Тогда запускаем:
Bash:
./Restler compile --api_spec swagger.json
Далее нужно запустить тестирование:
Bash:
./Restler fuzz --grammar_file Compile/grammar.py --dictionary_file Compile/dict.json --settings Compile/settings.json --time_budget 3600 --no_ssl
Что важно знать про RESTler:
- Он умеет работать с OAuth2, если вы передадите ему валидный токен в файле настроек (или через --token_refresh_cmd).
- Он поддерживает fuzzing mode и test mode (в test mode он просто прогоняет все валидные запросы, чтобы убедиться, что спецификация соответствует реальности).
- Он генерирует подробные отчеты в папке Fuzz - там будут лог-файлы с каждым запросом и ответом, а также список найденных ошибок (например, 500-е, таймауты).
- Основной недостаток: RESTler требователен к качеству спецификации. Если в OpenAPI неправильно указаны типы или отсутствуют параметры, фаззер может не нащупать реальные уязвимости. Также он не очень дружит с GraphQL.
2.2. Schemathesis: лёгкий, быстрый, с Hypothesis под капотом
Если вы работаете в Python-экосистеме, или просто не хотите возиться с компиляцией C#, то Schemathesis - ваш выбор. Это инструмент, который использует библиотеку Hypothesis для property-based тестирования. Он читает OpenAPI (или GraphQL) схему и генерирует тысячи тестовых случаев, которые соответствуют схеме, но с "интересными" значениями.Установка и первый запуск
Bash:
pip install schemathesis
Ссылка скрыта от гостей
), запускаем:
Bash:
schemathesis run https://example.com/swagger.json --base-url https://example.com/api/v1
- Любой статус 5xx (500, 502, 503 и т.д.)
- Любой статус 4xx, если он не описан в схеме как возможный ответ
- Таймауты
- Невалидный JSON в ответе (если ожидается JSON)
- Ошибки валидации схемы ответа
Глубже: как Schemathesis работает под капотом
Schemathesis использует стратегии Hypothesis для генерации данных. Например, для поля типа string с форматом email Hypothesis будет генерировать не только валидные email, но и "user@example" (без TLD), "user@example.com." (точка в конце), "user@example.com\r\n" (контрольные символы), "a@b.c" (минимальный), "user@example.com"*100 (длинный) и т.д. Это не случайные строки, а стратегии, основанные на свойствах формата.
Для чисел будут генерироваться граничные значения: 0, -1, максимальное для типа, float('inf'), float('nan'), и т.д. Для массивов - пустые, с одним элементом, с повторяющимися, с вложенными.
Кастомизация через Python-код
Одно из главных преимуществ Schemathesis - вы можете писать свои собственные стратегии и проверки, используя Python. Создайте файл test_api.py:
Python:
import schemathesis
from hypothesis import strategies as st
schema = schemathesis.from_uri("https://example.com/swagger.json")
@schema.parametrize()
def test_api(case):
# case - это сгенерированный запрос (метод, URL, заголовки, тело)
response = case.call()
# Дополнительные проверки
assert response.status_code < 500, f"Server error on {case.method} {case.path}"
# Проверка, что ответ содержит определенные поля
if case.method == "POST" and case.path == "/users":
assert "id" in response.json(), "Missing user ID in response"
Затем запускаете с помощью pytest:
Bash:
pytest test_api.py --schemathesis-base-url=https://example.com/api/v1
Что я люблю в Schemathesis:
- Он быстрый. На старте за пару минут покрывает все эндпоинты.
- Он интегрируется с pytest, что позволяет запускать тесты в CI/CD и получать стандартные отчеты.
- У него есть режим "stateful" (экспериментальный), который пытается строить последовательности, как RESTler, но пока сыроват. В большинстве случаев хватает stateless подхода, особенно если вы сами пишете сценарии.
- Отличная документация и активный community (спасибо @Stranger6667 и другим контрибьюторам).
На проекте с API для управления IoT-устройствами была спецификация, где поле firmware_version имело тип string с паттерном ^\d+\.\d+\.\d+$. Schemathesis сгенерировал значение "1.2.3.4" (четыре сегмента) и отправил на эндпоинт обновления прошивки. Сервер вернул 500, потому что код сравнивал версии, разбивая строку по точкам, и не ожидал лишнего сегмента. Это привело к крашу всего сервиса (необработанное исключение в основном потоке). Разработчики быстро поправили, но до этого ребята из QA не догадались проверить версию с четырьмя цифрами.
2.3. APIFuzzer: легкий мутационный фаззер на все случаи жизни
Иногда у вас нет OpenAPI-спецификации, или она настолько устарела, что лучше не смотреть. Или вам нужно быстро профаззить один эндпоинт с кастомными пейлоадами. Тут на сцену выходит APIFuzzer - небольшой Python-скрипт, который берет на вход JSON-файл с запросом и мутирует его по заданным правилам.Установка и базовая настройка
Bash:
git clone https://github.com/KissPeter/APIFuzzer.git
cd APIFuzzer
pip install -r requirements.txt
Теперь нужно подготовить файл с примером запроса, например request.json:
JSON:
{
"method": "POST",
"url": "https://example.com/api/v1/users",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer <token>"
},
"body": {
"name": "John Doe",
"email": "john@example.com",
"age": 30
}
}
Запускаем:
Bash:
python apifuzzer.py -f request.json -m mutation.json
JSON:
{
"mutations": [
{
"path": "body.name",
"type": "string",
"strategies": ["empty", "null", "long", "unicode", "sql", "xss"]
},
{
"path": "body.age",
"type": "integer",
"strategies": ["negative", "zero", "overflow", "string"]
},
{
"path": "body.email",
"type": "email",
"strategies": ["invalid", "long"]
}
]
}
APIFuzzer пройдется по каждому полю, применит указанные стратегии и отправит запросы. Он запишет ответы в лог и выделит те, которые привели к неожиданным статусам (например, 2xx там, где должен быть 4xx, или 5xx).
Почему APIFuzzer крут:
- Минимализм. Не нужно разбираться со сложными конфигами.
- Поддержка мутаций для JSON и XML. Можно фаззить как REST, так и SOAP.
- Легко расширять. Пишете свою стратегию на Python и добавляете в конфиг.
- Поддержка сессий. Можно передать cookie и токены, которые будут использоваться во всех запросах.
Условный случай из жизни:
В компании тестировали API банка. OpenAPI была, но она описывала только успешные ответы, а ошибки были недокументированы. С помощью APIFuzzer специалисты передали в поле amount строку "двести" вместо числа. API вернул 200 OK и списал деньги, хотя должен был выдать 400. Оказалось, что бэкенд использовал intval() в PHP, который преобразует строку в число, если она начинается с цифр. Но здесь строка не начиналась с цифр, и intval() вернул 0. В итоге списалось 0 рублей, но транзакция прошла успешно, и на счету пользователя сумма не изменилась. С точки зрения бизнеса - это уязвимость: можно было бы обнулить платежи, передавая строку. Классический сканер такое не нашел бы, потому что он не умеет менять типы данных в JSON.
3. Практика: REST API Fuzzing от А до Я
Теперь, когда мы познакомились с инструментами, давайте пройдем полный цикл: от подготовки до интерпретации результатов. Я покажу на примере Schemathesis, так как он наиболее универсален и дает быстрый результат. Но принципы применимы и к другим.3.1. Подготовка: получаем OpenAPI-спецификацию
Прежде чем запустить фаззинг, нужно иметь спецификацию. Если ее нет, можно:- Попросить разработчиков выгрузить из кода (например, с помощью Swashbuckle для .NET, или drf-yasg для Django).
- Использовать инструменты для reverse engineering, например, mitmproxy или Burp Suite для записи трафика и генерации OpenAPI из HAR-файлов (существуют конвертеры).
- Если API GraphQL - использовать introspection query (об этом позже).
Ссылка скрыта от гостей
. Запускаем:
Bash:
schemathesis run https://api.example.com/v1/swagger.yaml --base-url https://api.example.com/v1 --hypothesis-max-examples=1000 --report
3.2. Настройка проверок и исключений
Schemathesis по умолчанию использует набор проверок: not_a_server_error, status_code_conformance, response_schema_conformance и другие. Но иногда API возвращает 500 на запросы, которые и должны вызывать ошибку (например, при передаче неверного формата данных). Чтобы не засорять отчет ложными срабатываниями, можно отключить некоторые проверки:
Bash:
schemathesis run ... --checks all --exclude-checks response_schema_conformance
Bash:
schemathesis run ... --checks not_a_server_error,status_code_conformance
Bash:
schemathesis run ... --exclude-endpoints "/api/v1/legacy/.*"
Для более тонкой настройки используйте Python-скрипт, как я показывал ранее.
3.3. Интерпретация результатов: что считать проблемой
После прогона вы получите список "failed tests". Каждый тест - это конкретный запрос, который привел к неожиданному поведению. Теперь нужно разобраться, что реально является уязвимостью, а что просто "особенностью" API.Типичные результаты:
- 500 Internal Server Error - это потенциальная уязвимость. Может быть, в логах раскрывается стек трейс, или сервер падает. Даже если это просто ошибка валидации, она не должна быть 500. Нужно заводить баг.
- Таймаут (Timeout) - если запрос вызвал длительную обработку, возможно, это DoS-уязвимость. Особенно если запрос сгенерирован с очень большими данными или сложной структурой.
- Неожиданный статус 2xx - если запрос был заведомо невалидным (например, отсутствует обязательное поле), а сервер вернул 200, это может быть признаком того, что валидация не работает, и данные приняты. В некоторых случаях это может привести к повреждению данных или выполнению нежелательных действий.
- Нарушение схемы ответа - если ответ не соответствует OpenAPI-спецификации, это может быть безобидно (например, добавилось новое поле), а может указывать на утечку чувствительных данных (появилось поле password).
- 4xx, не описанный в схеме - если спецификация утверждает, что на ошибку валидации возвращается 400, а вы получили 409 Conflict, это не страшно, но стоит проверить, что код ошибки корректен.
Мой подход к триажу:
- Смотрю на статус ответа. Если 500 - иду в логи сервера (если есть доступ) и смотрю, что именно упало. Если упало с исключением, которое раскрывает стек трейс, - это критично. Если просто "Internal Server Error" без деталей - всё равно баг, но менее критичный.
- Если статус 200, а запрос был явно невалидным, проверяю, изменилось ли состояние системы. Например, после POST /users с полем email: "notanemail" вернулся 200 и создался пользователь. Это явно проблема валидации.
- Проверяю тело ответа на наличие чувствительных данных. Если фаззер подставил в user_id значение 1, и в ответе пришли данные другого пользователя - это IDOR (Insecure Direct Object Reference). Такие находки самые ценные.
- Измеряю время ответа. Если запрос с огромным массивом (например, 10,000 элементов) вызвал таймаут или время ответа 30 секунд, это потенциальный DoS.
3.4. Продвинутые техники: добавление аутентификации и сессий
Большинство API требуют аутентификации. Schemathesis поддерживает передачу заголовков через --headers или через Python-скрипт. Например:
Bash:
schemathesis run ... --header "Authorization: Bearer <token>"
Для stateful сценариев (например, когда после логина нужно использовать сессионную куку) я пишу фикстуры в pytest:
Python:
import pytest
import requests
import schemathesis
schema = schemathesis.from_uri("https://example.com/swagger.json")
@pytest.fixture(scope="session")
def auth_token():
# Получаем токен
response = requests.post("https://example.com/auth", json={"user": "test", "pass": "test"})
assert response.status_code == 200
return response.json()["token"]
@schema.parametrize()
def test_api(case, auth_token):
case.headers["Authorization"] = f"Bearer {auth_token}"
response = case.call()
# ...
4. GraphQL Fuzzing: отдельный разговор
GraphQL стал стандартом для многих современных API, и его особенности требуют отдельного подхода к фаззингу. В отличие от REST, где эндпоинты фиксированы, GraphQL имеет единую точку входа (обычно /graphql), а запросы определяются клиентом. Это открывает простор для атак, связанных с глубиной запросов, сложностью, интроспекцией и мутациями.4.1. Schema Discovery: как узнать, что скрывает GraphQL
Первым делом нужно получить схему. GraphQL поддерживает introspection query - стандартный запрос, который возвращает полную информацию о типах, полях, аргументах. Если интроспекция включена (а по умолчанию она включена в большинстве реализаций), вы можете запросить всё.Самый простой способ - использовать InQL (Burp Suite extension) или graphql-cop. Но я предпочитаю командную строку:
Bash:
# Сохраняем introspection query в файл introspection.graphql
cat > introspection.graphql <<EOF
query IntrospectionQuery {
__schema {
types {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}
}
EOF
# Отправляем запрос
curl -X POST https://example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query": "'"$(cat introspection.graphql | tr -d '\n')"'"}'
Ответ - огромный JSON, который можно сохранить и передать в инструменты.
Важно: Если интроспекция отключена, то вам придется восстанавливать схему вручную или через перехват трафика. Но многие разработчики забывают отключать интроспекцию в production, и это дает нам полную карту API.
4.2. Mutation Fuzzing: InQL и кастомные скрипты
Для фаззинга мутаций (изменяющих операций) я использую InQL в Burp Suite. Он умеет парсить схему и генерировать запросы с мутированными аргументами. Но мне не всегда удобно тащить Burp в CI/CD. Поэтому я написал небольшой скрипт на Python, который использует схему и генерирует мутации.Вот пример упрощенного генератора:
Python:
import json
import requests
from hypothesis import strategies as st, given
from hypothesis.strategies import builds, just
# Загружаем схему
with open("schema.json") as f:
schema = json.load(f)
# Для простоты, допустим, у нас есть мутация createUser с аргументами name и email
# Генерируем стратегии для аргументов
name_strategy = st.text(min_size=1, max_size=100)
email_strategy = st.emails()
@given(name=name_strategy, email=email_strategy)
def test_create_user(name, email):
mutation = """
mutation($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
"""
variables = {"name": name, "email": email}
response = requests.post("https://example.com/graphql", json={"query": mutation, "variables": variables})
# Проверки...
assert response.status_code == 200
# Проверяем, что ответ содержит id, и что пользователь создан
data = response.json()
assert "errors" not in data, f"GraphQL errors: {data['errors']}"
Запускаем этот тест через pytest с Hypothesis, и он сгенерирует тысячи вариаций аргументов. Это уже неплохо, но можно добавить мутации, которые выходят за пределы типов: например, передать в email строку из 10000 символов, или null, или массив.
Для более продвинутого фаззинга я использую библиотеку hypothesis-graphql, которая генерирует не только переменные, но и сами запросы (например, добавляет поля, которые не запрашивались, или вкладывает фрагменты).
4.3. Query Complexity: DoS через глубину и ширину
GraphQL позволяет клиенту запрашивать вложенные данные, и если разработчики не ограничивают сложность запросов, злоумышленник может отправить запрос, который вызовет огромную нагрузку на сервер. Классический пример:
Код:
GRAPHQL
query {
users {
posts {
comments {
author {
posts {
comments {
# ... и так далее
}
}
}
}
}
}
}
Такой запрос может привести к "n+1" проблемам и DoS. Фаззинг должен проверять как depth (глубину вложенности), так и width (количество полей на одном уровне).
Для тестирования я использую graphql-depth-limit или graphql-cost-analysis в своих скриптах, но с точки зрения фаззинга - я генерирую запросы с максимальной глубиной и шириной, и смотрю, не упал ли сервер или не превысил ли время ответа.
Пример генератора глубины с помощью hypothesis:
Python:
from hypothesis import strategies as st
def generate_deep_query(depth):
if depth == 0:
return "id"
return f"user {{ {generate_deep_query(depth-1)} }}"
# Генерируем глубину от 1 до 100
@given(depth=st.integers(min_value=1, max_value=100))
def test_deep_query(depth):
query = f"query {{ {generate_deep_query(depth)} }}"
response = requests.post("https://example.com/graphql", json={"query": query})
assert response.elapsed.total_seconds() < 5, "Query took too long"
assert response.status_code == 200
Если сервер позволяет глубину 100, и время ответа растет линейно или экспоненциально - это проблема. В идеале должен быть механизм ограничения глубины (например, 10-15 уровней).
4.4. Автоматизация GraphQL фаззинга в CI/CD
В отличие от REST, где мы просто запускаем schemathesis, для GraphQL нужно собирать схему. Я рекомендую:- В CI/CD (например, GitLab CI) на этапе тестирования сначала запускать скрипт, который выполняет introspection и сохраняет схему в файл.
- Затем запускать фаззинг с помощью Schemathesis (он поддерживает GraphQL через --graphql):
Bash:
schemathesis run https://example.com/graphql --graphql --base-url https://example.com
- Дополнительно, для более глубокого тестирования бизнес-логики, запускать кастомные скрипты, как описано выше.
5. CI/CD интеграция: делаем фаззинг обязательным
Теперь самое интересное: как встроить всё это в процесс разработки, чтобы каждый пул-реквест прогонялся через фаззинг, и если найдены критические проблемы, пайплайн падал. Это превращает безопасность из "мы проверили раз в полгода" в непрерывный процесс.5.1. GitLab CI: пример конфигурации
Допустим, у нас есть репозиторий с кодом API и OpenAPI-спецификация генерируется при сборке (например, с помощью swag для Go или drf-yasg для Django). Мы хотим запускать Schemathesis в CI.Создаем .gitlab-ci.yml:
YAML:
stages:
- test
- security
variables:
BASE_URL: "https://staging.example.com/api/v1"
schemathesis:
stage: security
image: python:3.11-slim
before_script:
- pip install schemathesis
script:
# Предполагается, что спецификация доступна по URL (например, после деплоя staging)
- schemathesis run https://staging.example.com/swagger.json
--base-url=$BASE_URL
--checks all
--hypothesis-max-examples=500
--report
--junit-xml=report.xml
artifacts:
reports:
junit: report.xml
paths:
- report.html
when: always
only:
- merge_requests
- main
Теперь на каждый MR будет запускаться фаззинг. Если будут найдены ошибки (проверки упадут), GitLab покажет это в MR.
5.2. Quality gate: когда фейлить пайплайн
Вопрос: на какие ошибки стоит фейлить пайплайн? Если фаззинг нашел 500-ю ошибку на эндпоинте, который обычно возвращает 200, это явно баг, и разработчик должен его исправить до мержа. Но бывают случаи, когда 500 возникает из-за внешних зависимостей (например, база данных временно недоступна), и фейлить пайплайн из-за этого неправильно.Мой подход:
- Сначала запускаю фаззинг в режиме dry-run (без остановки пайплайна) и сохраняю отчет.
- Если в отчете есть критические ошибки (например, 500 с раскрытием стека, или утечка данных), я останавливаю пайплайн.
- Для менее критичных ошибок (например, несоответствие схемы ответа) можно создать задачу в JIRA, но не блокировать мерж.
Bash:
schemathesis run ... --fail-on=500,timeout
5.3. Triage workflow: от findings до фикса
После того как фаззинг нашел баги, их нужно обработать. В идеале, автоматически создавать задачи в JIRA или Linear. У Schemathesis есть опция --output-junit-xml, которую мы уже используем. Можно написать простой скрипт, который парсит XML и через API создает задачи.Пример (на Python):
Python:
import xml.etree.ElementTree as ET
import requests
tree = ET.parse('report.xml')
root = tree.getroot()
for testcase in root.findall('.//testcase'):
if testcase.find('failure') is not None:
failure = testcase.find('failure')
message = failure.get('message')
# Создаем задачу в JIRA
payload = {
"fields": {
"project": {"key": "SEC"},
"summary": f"API Fuzzing finding: {testcase.get('name')}",
"description": f"Error: {message}\n\nSee report: {os.environ['CI_JOB_URL']}/artifacts/report.html",
"issuetype": {"name": "Bug"}
}
}
requests.post("https://jira.example.com/rest/api/2/issue", json=payload, auth=('user', 'token'))
После того как разработчик исправит баг, вы можете перезапустить фаззинг (например, по комментарию в задаче). Это замыкает цикл.
Заключение: фаззинг как образ жизни
Мы прошли долгий путь. От теории, через инструменты, до практической интеграции. Но я хочу донести до вас одну важную мысль: API Fuzzing - это не просто запуск инструмента и просмотр отчета. Это процесс, который требует понимания того, как работает ваше приложение, какие у него есть зависимости, и где могут быть скрытые баги.Я помню, как в начале своего пути я запускал фаззеры "по-быстрому", получал кучу ложных срабатываний и думал, что это бесполезно. Но потом я понял: нужно настраивать инструменты под свой контекст, писать кастомные проверки, учиться читать результаты. И когда ты это делаешь, фаззинг начинает приносить такие находки, которые не найдет ни один сканер, ни один пентестер вручную.
Особенно ценен фаззинг в CI/CD, где он работает на каждом изменении. Это как иметь персонального хакера, который не устает, не спит и всегда пытается сломать твое API. И когда разработчик видит, что его пул-реквест упал из-за того, что он забыл про валидацию email, он начинает думать о безопасности на этапе написания кода. А это и есть главная цель - сдвинуть безопасность влево.