Перейти к содержимому

Git bisect: поиск уязвимого коммита в open-source проекте перед пентестом кода

Git bisect: поиск уязвимого коммита в open-source проекте перед пентестом кода
Время чтения: 12 мин.

Вот иллюстративный сценарий, смоделированный по мотивам типичных уязвимостей в open-source Rust-библиотеках: макрос derive(Deserialize) генерирует код десериализации, не проверяющий инвариант длины буфера. Поле data типа Vec<T> должно содержать ровно nrows * ncols элементов, но автоматическая реализация принимает произвольные значения — и допускает чтение памяти за пределами аллокации. Патч уже в мастере, но для формирования advisory нужно установить затронутые версии. Репозиторий — почти две тысячи коммитов. Перебирать руками? git bisect сводит поиск уязвимого коммита к одиннадцати проверкам вместо линейного просмотра всей истории. Конкретные хеши коммитов и детали далее приведены для демонстрации методики и могут не соответствовать реальной истории какого-либо репозитория.

Git для пентестера: зачем восстанавливать историю уязвимости

Первый вопрос после обнаружения уязвимости в зависимости — с какой версии она существует? Ответ определяет окно экспозиции: сколько времени каждый пользователь библиотеки был под ударом. Для формализованных advisory — GHSA (GitHub Security Advisory, стандартизированное уведомление об уязвимости в open-source пакете) или CVE (Common Vulnerabilities and Exposures, публичный идентификатор уязвимости) — нужно заполнить поле «affected versions». Без точного диапазона версий advisory неполон: сканеры зависимостей вроде Dependabot, Snyk или Trivy не смогут корректно предупредить разработчиков.

Теперь про контекст угрозы. В MITRE ATT&CK — открытой базе тактик и техник атак, где каждая техника имеет T-код (T1190, T1195 и т.п.) — атаки через уязвимые зависимости описаны техникой T1195.001 (Compromise Software Dependencies and Development Tools). Если коротко: атакующий находит уязвимую стороннюю зависимость в вашем проекте и эксплуатирует её. Сам процесс поиска уязвимого коммита через git bisect и составления advisory — defensive-активность, но T1195.001 описывает тот вектор атаки, от которого эта работа защищает. Смежная T1195.002 (Compromise Software Supply Chain) — про компрометацию самого процесса сборки или поставки ПО (как в случае SolarWinds), что выходит за рамки нашего сценария. Обе относятся к тактике Initial Access. Параллельно работает T1588.006 (Vulnerabilities, тактика Resource Development) — получение или поиск уязвимостей для использования в ходе атаки.

OWASP (Open Worldwide Application Security Project — открытый проект по безопасности приложений) выделяет проблему уязвимых компонентов в категорию A06:2021 — Vulnerable and Outdated Components: использование библиотек и фреймворков с известными уязвимостями. Рядом — A08:2021 — Software and Data Integrity Failures: код без защиты от нарушений целостности, включая небезопасную десериализацию, как в нашем примере с nalgebra.

Где git bisect встраивается в процесс пентеста кода:

  1. Получение исходного кода — клонирование репозитория (white box или grey box сценарий)
  2. Идентификация уязвимости — SAST (статический анализ: semgrep, bandit), ручной review или данные из публичных advisory
  3. Определение окна экспозиции — git bisect для поиска коммита, внёсшего уязвимость
  4. Маппинг на версии — привязка найденного коммита к релизным тегам через git tag --contains
  5. Отчёт или advisory — затронутые версии, рекомендации по обновлению, оценка импакта

Без третьего шага отчёт выглядит так: «уязвимость есть». Но нет ответа на «как давно?» и «кто ещё затронут?». А это — ровно то, что нужно для incident response.

Как git bisect находит коммит с уязвимостью

git bisect — бинарный поиск по истории коммитов. Алгоритм принимает два ориентира: коммит, где уязвимости ещё нет (в терминах bisect — good), и коммит, где она уже есть (bad). Git выбирает коммит посередине диапазона, переключает рабочую директорию на него (делает checkout — переход к определённому состоянию кода) и ждёт решения. Вы проверяете, присутствует ли уязвимый паттерн, и сообщаете результат: git bisect good или git bisect bad. Git сужает диапазон вдвое и повторяет — пока не останется один коммит.

Сложность — O(log N):

  • 100 коммитов — около 7 проверок
  • 1 000 коммитов — около 10 проверок
  • 2 000 коммитов — около 11 проверок
  • 1 000 000 коммитов — около 20 проверок

Линейный просмотр тех же 2 000 коммитов — в среднем 1 000 проверок. Разница ощутимая.

Отличие от разработческого использования: разработчик обычно проверяет «ломается тест или нет». При secure code review проверка звучит иначе: «присутствует ли уязвимый паттерн в коде». Проверкой может быть запуск semgrep (статический анализатор с поддержкой custom-правил на YAML) с правилом на небезопасную десериализацию, поиск строки через rg (ripgrep — быстрый grep, написанный на Rust) или запуск юнит-теста, воспроизводящего уязвимость.

Secure code review: поиск бага в git истории шаг за шагом

Требования к окружению

  • Git: версия 2.x и выше. Проверка: git --version
  • Терминал: bash, zsh или совместимый shell. На Windows — WSL или Git Bash
  • Клонированный репозиторий целевого проекта с полной историей. Shallow clone (--depth N) не подходит: bisect не увидит старые коммиты
  • ripgrep для быстрого поиска по кодовой базе: apt install ripgrep (Debian/Ubuntu) или brew install ripgrep (macOS)
  • Для автоматизации (опционально): semgrep (pip install semgrep) или bandit (pip install bandit) — анализатор безопасности Python-кода
  • RAM: сам git bisect оперирует метаданными и не требует много памяти, но проверка на каждом шаге (сборка, тесты) может потребовать значительно больше ресурсов для крупных проектов
  • Чистое рабочее дерево: убедитесь, что git status показывает nothing to commit, working tree clean. Есть незакоммиченные изменения — git stash перед стартом

Делай раз, делай два, делай три

Разберём поиск уязвимого коммита на иллюстративном примере, смоделированном по мотивам публичных advisory для библиотеки nalgebra. Конкретные хеши коммитов и детали ниже приведены для демонстрации методики и могут не соответствовать реальной истории репозитория. Задача: найти коммит, в котором появилась автоматически генерируемая реализация Deserialize без валидации инвариантов структуры данных.

Шаг 1. Определите границы поиска. Начните с коммита, где уязвимость точно есть. Если фикс уже известен — возьмите коммит непосредственно перед ним: символ ^ в git-нотации означает «родительский коммит». Для нашего случая фикс — коммит 5bff536, значит 5bff536^ — последний уязвимый. Для «хорошего» коммита подойдёт самый ранний релизный тег или — если нет точной информации — самый первый коммит репозитория.

Шаг 2. Запустите bisect и задайте границы.

git clone https://github.com/dimforge/nalgebra.git
cd nalgebra/
git bisect start
git bisect bad 5bff536^
git bisect good $(git rev-list --max-parents=0 HEAD | head -1)

Команда git rev-list --max-parents=0 HEAD возвращает хеш(и) корневых коммитов в истории; head -1 берёт один из них. На этом этапе HEAD (указатель на текущую ветку или конкретный коммит в состоянии detached HEAD, как во время bisect) уже указывает на bad-коммит (после git bisect bad), поэтому rev-list от HEAD корректно находит корни всей релевантной истории. Если в репозитории несколько корневых коммитов (например, после merge независимых веток) — убедитесь, что выбран нужный корень. Ожидаемый вывод: Bisecting: 980 revisions left to test after this (roughly 10 steps). Git автоматически переключит рабочую директорию на коммит в середине диапазона.

Шаг 3. На каждом шаге проверяйте наличие уязвимого паттерна. Выполните rg 'struct VecStorage' -B 1 (флаг -B 1 показывает одну строку выше найденного совпадения). Если в выводе видна строка derive(Serialize, Deserialize) над определением структуры — коммит уязвим, выполняйте git bisect bad. Если структуры VecStorage нет в коде или Deserialize отсутствует — git bisect good. После каждой пометки Git обновляет диапазон, показывает Bisecting: N revisions left... и переключает на следующий коммит. Повторяйте до появления сообщения <hash> is the first bad commit.

Шаг 4. Обработайте краевые случаи. Bisect может привести к коммиту переименования вместо реального внесения уязвимости. В нашем случае первый результат — коммит 0f66403 с сообщением Rename MatrixVec to VecStorage. Структура была переименована, но уязвимый derive существовал и раньше, под старым именем. Решение: git bisect reset, затем git checkout 0f66403, новый bisect с паттерном rg 'struct MatrixVec' -B 1. Итог — коммит 086e6e7 от февраля 2017 года.

Шаг 5. Привяжите коммит к версиям. Команда git tag --contains 086e6e7 покажет все теги (релизы), содержащие виновный коммит — это затронутые версии. Обратная операция git tag --no-contains 086e6e7 — версии до внесения уязвимости. Эти данные идут в поле affected / unaffected advisory. Выполните git show 086e6e7 для просмотра полного diff.

Шаг 6. Завершите bisect. Обязательно выполните git bisect reset — команда возвращает HEAD туда, где вы были до старта. Забудете — продолжите работать на промежуточном коммите, и потом будете разбираться, почему код «сломался».

Git bisect для безопасности: автоматизация поиска регрессии

Ручной bisect требует на каждом шаге запускать проверку и интерпретировать результат. git bisect run принимает скрипт и прогоняет весь цикл без вашего участия. Правила для скрипта:

  • Код возврата 0 — коммит хороший (уязвимости нет)
  • Код возврата 1–124 или 126–127 — коммит плохой (уязвимость присутствует)
  • Код возврата 125 — коммит невозможно проверить (файл отсутствует, проект не собирается), пропустить

Пример скрипта для автоматического поиска уязвимого паттерна десериализации:

#!/bin/bash
# check_vuln.sh — скрипт для git bisect run
if ! [ -f "src/base/vec_storage.rs" ]; then
  exit 125  # файл не существует в этой версии, пропускаем
fi
if rg -q 'derive.*Deserialize' src/base/vec_storage.rs; then
  exit 1    # паттерн найден — коммит уязвим
fi
exit 0      # паттерн отсутствует — коммит чист

Сделайте скрипт исполняемым через chmod +x check_vuln.sh, затем после установки границ bisect выполните git bisect run ./check_vuln.sh. Git прогонит весь бинарный поиск автоматически — на ripgrep каждый шаг занимает доли секунды, весь bisect завершается за секунды даже на двух тысячах коммитов.

Для Python-проектов вместо ripgrep подойдёт bandit: скрипт вызывает bandit -r src/ -t B301 (B301 — правило на небезопасный pickle), и ненулевой код возврата bandit автоматически помечает коммит как плохой. Для Node.js аналогичную роль играют ESLint security-плагины. Принцип тот же: любая команда, различающая «уязвимо» и «безопасно» через код возврата, совместима с git bisect run.

Важный момент: если сборка проекта падает на промежуточном коммите, возвращайте 125 (skip), а не 1 (bad). Невозможность собрать код — не признак уязвимости, а техническая проблема конкретного состояния репозитория. При ручной проверке вместо 125 используйте команду git bisect skip — Git попробует соседний коммит.

При ручном подходе — 2 минуты на шаг, 11 шагов — это 22 минуты живого времени. Автоматизированный скрипт с rg отрабатывает тот же сценарий за секунды. На крупных репозиториях с сотнями тысяч коммитов разница ещё драматичнее.

Git blame vs bisect: анализ коммитов open-source

git bisect — не единственный инструмент для работы с историей кода при security review. Три подхода решают разные задачи:

Инструмент Что делает Когда использовать
git bisect Находит коммит, внёсший изменение Знаете «до» и «после», но не знаете «когда именно»
git blame Показывает автора и коммит для каждой строки файла Уже нашли уязвимый код, нужен контекст авторства
git log -S / -G Ищет коммиты, где строка появилась или исчезла Отслеживаете историю конкретного паттерна или функции

git blame выводит построчную аннотацию файла: для каждой строки — хеш коммита, автор и дата последнего изменения. Выполните git blame src/base/vec_storage.rs, чтобы увидеть, кто и когда добавил строку с derive(Deserialize). После bisect это даёт контекст: прошёл ли код ревью, есть ли паттерн — один и тот же разработчик систематически вносит уязвимые конструкции.

git log -S (pickaxe search) находит коммиты, в которых указанная строка была добавлена или удалена. Команда git log -S 'derive(Deserialize)' -- src/ покажет все коммиты, где эта строка появлялась или исчезала в каталоге src/. Флаг -G принимает регулярное выражение: git log -G 'derive.*Deserialize' --oneline найдёт коммиты с любым вариантом derive-макроса, содержащим Deserialize. Pickaxe полезен, когда нужно быстро получить «историю жизни» конкретного опасного паттерна в кодовой базе без полного bisect-прогона.

Комбинированный workflow для security review:

  1. git log -S 'опасный_паттерн' — получить список коммитов, связанных с подозрительным кодом
  2. git bisect — точно определить первый коммит с уязвимостью, когда pickaxe даёт слишком много результатов или паттерн претерпевал рефакторинг
  3. git blame — после нахождения коммита изучить авторство и контекст каждой строки
  4. git show <hash> — просмотреть полный diff виновного коммита, оценить объём изменений

За минуты вы проходите от «где-то в зависимости есть уязвимость» до «вот коммит, вот автор, вот затронутые версии, вот оценка того, прошёл ли изменённый код ревью».

Ограничения git bisect при поиске уязвимого коммита

Переименование и рефакторинг. Если уязвимый код был переименован (как MatrixVec в VecStorage), bisect приведёт к коммиту переименования, а не к реальному внесению проблемы. Автоматизированный скрипт должен учитывать все варианты имени — либо потребуется перезапуск с новым паттерном.

Merge-коммиты. В репозиториях с активным merge workflow бинарный поиск проходит через merge-коммиты, которые сами не содержат изменений кода. Флаг --first-parent при старте (git bisect start --first-parent) ограничивает поиск основной линией истории, игнорируя ответвления.

Нестабильные проверки. Если проверка даёт разный результат на одном и том же коммите (зависимость от внешнего сервиса, race condition в тестах), bisect придёт к ложному результату. Решение: максимально детерминированная проверка — поиск статического паттерна в коде через rg, а не runtime-тест с сетевыми зависимостями.

Несобирающиеся коммиты. Ручной режим — git bisect skip. Автоматизация — код возврата 125. При большом количестве skip подряд Git вместо одного коммита выдаст диапазон: «виновник где-то между commit A и commit B» — и дальше придётся разбираться руками.

Уязвимости без бинарного сигнала. Bisect требует однозначного «уязвим / не уязвим». Для проблем вроде недостаточной энтропии генератора случайных чисел или race condition формализовать проверку в скрипт бывает невозможно. В таких случаях git log -S по имени подозрительной функции часто работает быстрее.

Масштаб. Для монорепозиториев с сотнями тысяч коммитов ограничьте bisect конкретным путём: git bisect start -- src/crypto/. Git будет рассматривать только коммиты, затрагивающие файлы в указанном каталоге, — это существенно сокращает пространство поиска.

Навык работы с git bisect в контексте безопасности разделяет два типа отчётов. Первый: «обнаружена небезопасная десериализация в endpoint /api/data». Второй: «уязвимость внесена коммитом abc123 от 12.03.2023, затронуты версии 1.2–1.5, окно экспозиции — 14 месяцев, code review на PR не проводился». Для заказчика разница принципиальна — второй вариант даёт данные для incident response, первый оставляет с вопросом «и что теперь?».

Большинство пентестеров работают с кодом как с чёрным ящиком: текущая версия, уязвимость, рекомендация. История коммитов остаётся за скобками. В white box и grey box сценариях — а это большинство корпоративных проектов — git-история содержит контекст, который влияет на приоритизацию находки. Кто добавил уязвимый код, прошёл ли он ревью, были ли неудачные попытки исправления — всё это меняет risk rating в итоговом отчёте и оценку зрелости процессов разработки у заказчика.

Я пришёл в безопасность из разработки и первые полгода использовал git только для clone и pull. Bisect, blame, log -S — три команды, которые вместе дают полную картину жизни уязвимости в кодовой базе. Осваивать их лучше до того, как столкнётесь с задачей «найди, когда это сломалось» на реальном проекте с дедлайном. На курсе IB Basics от Codeby Academy такие фундаментальные пробелы закрываются за пару месяцев — без самостоятельного собирания программы из разрозненных туториалов.

Эту тему и смежные навыки разбирают на практике в курсе «Git. Система контроля версий» Codeby Academy.