Статья API Fuzzing: Автоматизация тестирования безопасности API с помощью Fuzzing-инструментов

1774208175951.webp
Я вам расскажу, как мы в 2026-м году наконец-то перестали молиться на сканеры, которые пихают ' OR 1=1-- в каждый параметр, и начали делать нормальную безопасность API. Ту самую, где ты не гадаешь на кофейной гуще, а реально видишь, как твой эндпоинт падает от переполнения буфера в user_id, или как база данных захлебывается от вложенных мутаций GraphQL.

Если вы до сих пор думаете, что 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 корзина обновлялась, и сумма заказа уменьшалась. Можно было купить товар с отрицательной ценой.
Видите? Это не "OR 1=1", это реальные баги, которые проходят сквозь стандартные сканеры, потому что они требуют понимания контекста.

И вот тут возникает ключевое различие: 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
Эта команда создаст папку Compile с набором скомпилированных файлов - там будут все эндпоинты, типы данных, зависимости. На выходе вы получите grammar.py и dict.json, которые описывают структуру API.

Далее нужно запустить тестирование:

Bash:
./Restler fuzz --grammar_file Compile/grammar.py --dictionary_file Compile/dict.json --settings Compile/settings.json --time_budget 3600 --no_ssl
Здесь --time_budget - время фаззинга в секундах (я ставлю 3600 для старта). --no_ssl отключает проверку SSL (если у вас самоподписанные сертификаты). RESTler начнет генерировать запросы, учитывая зависимости. Он будет пытаться пройти по всем путям: авторизация, создание ресурсов, обновление, удаление.

Что важно знать про RESTler:
  • Он умеет работать с OAuth2, если вы передадите ему валидный токен в файле настроек (или через --token_refresh_cmd).
  • Он поддерживает fuzzing mode и test mode (в test mode он просто прогоняет все валидные запросы, чтобы убедиться, что спецификация соответствует реальности).
  • Он генерирует подробные отчеты в папке Fuzz - там будут лог-файлы с каждым запросом и ответом, а также список найденных ошибок (например, 500-е, таймауты).
  • Основной недостаток: RESTler требователен к качеству спецификации. Если в OpenAPI неправильно указаны типы или отсутствуют параметры, фаззер может не нащупать реальные уязвимости. Также он не очень дружит с GraphQL.
RESTler отлично находит race conditions и проблемы с авторизацией. Ему можно скормить спецификацию внутреннего API, и он за 20 минут найдёт, что при последовательном вызове POST /orders и GET /orders/{id} можно было получить заказ другого пользователя, если подменить ID в пути, хотя в спецификации было указано, что доступ только к своим. Разрабы могут забыть добавить проверку владельца в контроллере. RESTler просто подставит ID из предыдущего ответа (который был создан в рамках той же сессии) и вы получите доступ.

2.2. Schemathesis: лёгкий, быстрый, с Hypothesis под капотом​

Если вы работаете в Python-экосистеме, или просто не хотите возиться с компиляцией C#, то Schemathesis - ваш выбор. Это инструмент, который использует библиотеку Hypothesis для property-based тестирования. Он читает OpenAPI (или GraphQL) схему и генерирует тысячи тестовых случаев, которые соответствуют схеме, но с "интересными" значениями.

Установка и первый запуск

Bash:
pip install schemathesis
Теперь, имея OpenAPI-спецификацию (допустим, ), запускаем:

Bash:
schemathesis run https://example.com/swagger.json --base-url https://example.com/api/v1
Schemathesis начнет генерировать запросы для каждого эндпоинта, используя Hypothesis для поиска контрпримеров. Он будет отправлять их на --base-url и анализировать ответы. По умолчанию он считает ошибкой:
  • Любой статус 5xx (500, 502, 503 и т.д.)
  • Любой статус 4xx, если он не описан в схеме как возможный ответ
  • Таймауты
  • Невалидный JSON в ответе (если ожидается JSON)
  • Ошибки валидации схемы ответа
Вы можете настроить эти проверки через параметры --checks.

Глубже: как 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
Это дает невероятную гибкость. Вы можете добавить проверки бизнес-логики, например, что после удаления ресурса он становится недоступным, или что поле balance всегда неотрицательное.

Что я люблю в Schemathesis:
  • Он быстрый. На старте за пару минут покрывает все эндпоинты.
  • Он интегрируется с pytest, что позволяет запускать тесты в CI/CD и получать стандартные отчеты.
  • У него есть режим "stateful" (экспериментальный), который пытается строить последовательности, как RESTler, но пока сыроват. В большинстве случаев хватает stateless подхода, особенно если вы сами пишете сценарии.
  • Отличная документация и активный community (спасибо @Stranger6667 и другим контрибьюторам).
Пример бага, найденного Schemathesis:

На проекте с 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
Где 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 лучше использовать Schemathesis, но как дополнение APIFuzzer очень полезен.

Условный случай из жизни:

В компании тестировали API банка. OpenAPI была, но она описывала только успешные ответы, а ошибки были недокументированы. С помощью APIFuzzer специалисты передали в поле amount строку "двести" вместо числа. API вернул 200 OK и списал деньги, хотя должен был выдать 400. Оказалось, что бэкенд использовал intval() в PHP, который преобразует строку в число, если она начинается с цифр. Но здесь строка не начиналась с цифр, и intval() вернул 0. В итоге списалось 0 рублей, но транзакция прошла успешно, и на счету пользователя сумма не изменилась. С точки зрения бизнеса - это уязвимость: можно было бы обнулить платежи, передавая строку. Классический сканер такое не нашел бы, потому что он не умеет менять типы данных в JSON.


3. Практика: REST API Fuzzing от А до Я​

Теперь, когда мы познакомились с инструментами, давайте пройдем полный цикл: от подготовки до интерпретации результатов. Я покажу на примере Schemathesis, так как он наиболее универсален и дает быстрый результат. Но принципы применимы и к другим.

3.1. Подготовка: получаем OpenAPI-спецификацию​

Прежде чем запустить фаззинг, нужно иметь спецификацию. Если ее нет, можно:
  1. Попросить разработчиков выгрузить из кода (например, с помощью Swashbuckle для .NET, или drf-yasg для Django).
  2. Использовать инструменты для reverse engineering, например, mitmproxy или Burp Suite для записи трафика и генерации OpenAPI из HAR-файлов (существуют конвертеры).
  3. Если API GraphQL - использовать introspection query (об этом позже).
Допустим, у нас есть swagger.yaml. Мы хотим протестировать API, работающий на . Запускаем:

Bash:
schemathesis run https://api.example.com/v1/swagger.yaml --base-url https://api.example.com/v1 --hypothesis-max-examples=1000 --report
Опция --hypothesis-max-examples=1000 означает, что Hypothesis попытается сгенерировать до 1000 примеров на каждую операцию (можно увеличивать, если нужно больше). --report создаст HTML-отчет.

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
Если вы знаете, что определенный эндпоинт всегда возвращает 200, даже на неверные данные (бывает такое в legacy), можно добавить его в список игнорирования:

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 - это баги безопасности. Иногда это просто недоработка, которая не дает злоумышленнику ничего, кроме информации о том, что сервер упал. Но даже это может быть использовано для fingerprinting. Поэтому всегда нужно анализировать контекст.

Мой подход к триажу:
  1. Смотрю на статус ответа. Если 500 - иду в логи сервера (если есть доступ) и смотрю, что именно упало. Если упало с исключением, которое раскрывает стек трейс, - это критично. Если просто "Internal Server Error" без деталей - всё равно баг, но менее критичный.
  2. Если статус 200, а запрос был явно невалидным, проверяю, изменилось ли состояние системы. Например, после POST /users с полем email: "notanemail" вернулся 200 и создался пользователь. Это явно проблема валидации.
  3. Проверяю тело ответа на наличие чувствительных данных. Если фаззер подставил в user_id значение 1, и в ответе пришли данные другого пользователя - это IDOR (Insecure Direct Object Reference). Такие находки самые ценные.
  4. Измеряю время ответа. Если запрос с огромным массивом (например, 10,000 элементов) вызвал таймаут или время ответа 30 секунд, это потенциальный DoS.

3.4. Продвинутые техники: добавление аутентификации и сессий​

Большинство API требуют аутентификации. Schemathesis поддерживает передачу заголовков через --headers или через Python-скрипт. Например:

Bash:
schemathesis run ... --header "Authorization: Bearer <token>"
Но если токен протухает, нужно обновлять. Можно написать скрипт, который перед запуском получает свежий токен. Или использовать --auth для Basic Auth.

Для 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 нужно собирать схему. Я рекомендую:
  1. В CI/CD (например, GitLab CI) на этапе тестирования сначала запускать скрипт, который выполняет introspection и сохраняет схему в файл.
  2. Затем запускать фаззинг с помощью Schemathesis (он поддерживает GraphQL через --graphql):

Bash:
schemathesis run https://example.com/graphql --graphql --base-url https://example.com
Schemathesis автоматически выполнит introspection и начнет фаззинг. Это самый простой путь.

  1. Дополнительно, для более глубокого тестирования бизнес-логики, запускать кастомные скрипты, как описано выше.

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, но не блокировать мерж.
В Schemathesis можно задать порог ошибок через --fail-on:

Bash:
schemathesis run ... --fail-on=500,timeout
Но это всё или ничего. Для более тонкого контроля я пишу скрипт, который парсит JUnit XML и решает, фейлить или нет.

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, он начинает думать о безопасности на этапе написания кода. А это и есть главная цель - сдвинуть безопасность влево.
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab