Прямой ответ
Фаззинг — автоматическая подача на вход программе большого количества неожиданных данных с целью вызвать сбой. В отличие от тестов, фаззер не знает ожидаемого результата: он ищет состояния, которые программа не должна допускать, — необработанные исключения, падения, зависания, исчерпание памяти.
Современные фаззеры работают по принципу coverage-guided: они отслеживают, какие участки кода выполняет очередной вход, и отдают приоритет тем мутациям, которые открывают новые пути. Это позволяет проникать глубоко в логику парсинга, куда случайный перебор не доходит.
Atheris — фаззер для Python от Google, построенный поверх libFuzzer. Он инструментирует байт-код Python и собирает покрытие на уровне интерпретатора, поэтому работает и с чистым Python, и с нативными расширениями.
import sys, atheris
def parse_config(data: bytes) -> dict:
text = data.decode("utf-8", errors="replace")
out = {}
for line in text.splitlines():
if "=" not in line:
continue
k, _, v = line.partition("=")
out[k.strip()] = v.strip()
return out
def TestOneInput(data: bytes) -> None:
# падение с необработанным исключением фаззер считает находкой
parse_config(data)
if __name__ == "__main__":
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()
Ключевые элементы:
- Целевая функция (
TestOneInput) получает байты и должна отработать без необработанного исключения. - Корпус (corpus) — каталог стартовых примеров. Фаззер мутирует их, а найденные падения сохраняет отдельно.
- FuzzedDataProvider — помощник для превращения байтов в структурированные значения: строки, числа, списки, выборы из вариантов.
- Санитайзеры — для нативного кода подключаются санитайзеры адресов и неопределённого поведения; в чистом Python основную роль играет перехват исключений.
def TestOneInput(data: bytes) -> None:
fdp = atheris.FuzzedDataProvider(data)
n = fdp.ConsumeIntInRange(0, 5)
items = [fdp.ConsumeUnicodeNoSurrogates(64) for _ in range(n)]
limit = fdp.ConsumeFloat()
build_report(items, limit=limit) # проверка на любых комбинациях
Практика: что фаззить и как организовать
1. Лучшие цели — парсеры и десериализаторы. Загрузчики конфигураций, декодеры форматов, обработчики входящих payloads, функции разбора пользовательского ввода. Это код, который по определению работает с недоверенными данными.
2. Дайте фаззеру стартовый корпус. Без примеров он тратит время на тривиальные входы. Положите в корпус валидные документы, пограничные случаи и уже известные проблемные значения.
3. Разделяйте «найденное» и «воспроизводимое». Найденный краш — это файл в каталоге падений. Его сохраняют в репозиторий и превращают в обычный тест, чтобы регрессия не вернулась.
4. Проверяйте не только падения. Полезно добавлять собственные инварианты внутрь целевой функции: «после парсинга и сериализации структура совпадает», «сумма не отрицательна». Тогда фаззер ищет нарушения свойств, а не только исключения. Этот приём сближает фаззинг с property-based тестированием.
5. Ограничивайте время и ресурсы. Фаззинг — процесс без естественного завершения. В CI запускают с лимитом (-max_total_time=300), отдельную долгую сессию — ночью или на отдельной машине.
6. Помните про безопасность. Обработка недоверенного ввода — основной источник инъекций; фаззинг дополняет, но не заменяет безопасное кодирование — см. инъекции в Python и защиту от SQL-инъекций.
Рекомендации:
- Начинайте с одной функции. Фаззинг «всего приложения» даёт шум; одна точка входа с недоверенными данными — измеримый результат.
- Ловите и классифицируйте исключения. Необработанное исключение в библиотеке — почти всегда дефект обработки входа, даже если оно не приводит к падению процесса.
- Следите за потреблением памяти. Фаззер находит бесконечное разрастание структур: вход из 10 байт, порождающий гигабайт данных, — типичная находка.
- Проверяйте зависания. Таймаут на входе — такая же находка, как краш: это готовый вектор отказа в обслуживании.
- Не фаззьте код с внешними вызовами. Сеть и диск замедляют прогон в сотни раз и дают ложные срабатывания. Внешние зависимости подменяют двойниками — см. тестовые двойники.
Практический сценарий: фаззер нашёл отказ в обслуживании в парсере импорта
Контекст: сервис принимал CSV-файлы от клиентов и разбирал их собственной функцией. Файлы проходили проверку размера до 10 МБ.
- Цель. Фаззингу подвергли функцию разбора строки CSV, подавая на вход произвольные байты через
FuzzedDataProvider. - Находка 1. На входе из 4 КБ функция выделяла более 700 МБ: квадратные скобки в значении трактовались как начало вложенной структуры, которая разворачивалась рекурсивно.
- Находка 2. Строка из 200 повторяющихся разделителей вызывала квадратичное поведение: разбор одного файла занимал 40 секунд, что при десятке параллельных загрузок исчерпывало воркеры.
- Действия. (а) Рекурсивный разбор ограничили глубиной и общим числом элементов; (б) квадратичный участок заменили на однократное разбиение; (в) добавили лимит на число полей в строке; (г) оба найденных входа сохранили в репозиторий как регрессионные тесты.
- Проверка. Повторный прогон фаззинга в течение часа новых падений не дал; нагрузочная проверка на найденных входах показала время разбора менее 20 мс.
Вывод: оба дефекта были недоступны ни ручному тестированию, ни проверке размера файла. Стоимость находки — час фаззинга одной функции.
FAQ
Чем фаззинг отличается от property-based тестирования? PBT генерирует структурированные входы по заданным стратегиям и проверяет сформулированные свойства. Фаззер мутирует байты, опираясь на покрытие кода, и ищет сбои без заранее заданных свойств. Методы дополняют друг друга: PBT знает, что должно быть верно, фаззинг находит, где это не так.
Нужен ли фаззинг, если есть тесты? Тесты проверяют известные сценарии. Фаззинг исследует пространство входов, которое человек не перебирает. Практика показывает, что находки чаще всего появляются в коде, работающем с внешними данными.
Можно ли фаззить HTTP-эндпоинт целиком? Можно, но это дорого и шумно: много времени уходит на транспорт. Эффективнее подавать случайные данные напрямую в функцию обработки тела запроса, минуя сервер.
Что делать с найденным падением? Сохранить вход в репозиторий, добавить регрессионный тест, исправить обработку и проверить, что класс входа больше не приводит к сбою. Хранение найденных входов — часть ценности фаззинга: это готовый набор сложных случаев.
Источники: Atheris (проект Google), LLVM libFuzzer docs, OWASP (fuzzing) — ссылки в шапке.