Когда тире становится оружием: история одной уязвимости
Представьте: вы написали CI-скрипт, который автоматически добавляет коммиты с сообщением, взятым из пользовательского ввода. Или, скажем, у вас есть Git-хук, принимающий имя файла от внешнего сервиса. Всё работает годами. А потом один хитрый пользователь вводит --help вместо названия — и ваш скрипт выводит справку Git вместо обработки данных. Ещё хуже: он передаёт --exec=malicious.sh — и злоумышленник получает удалённый доступ.
Это не теоретическая атака. В июле 2026 года разработчики Git наконец-то обратили внимание на давнюю проблему безопасности, связанную с инъекцией опций через пользовательский ввод. Решение? Флаг --end-of-options (или -- в старом синтаксисе). Но не всё так просто, как кажется.
В сегодняшней статье мы разберём, как работает новый механизм, почему старый -- уже не спасает, и как правильно защитить свои Git-скрипты от «отравленных» аргументов. Материал основан на подробном техническом разборе от Nesbitt Источник, а также на официальной документации Git 2.47+.
Старый добрый --: почему он перестал работать?
Если вы хоть раз писали bash-скрипты, вы знаете магию двойного тире --. По соглашению POSIX, -- означает «всё, что дальше — это аргументы, а не опции». Git поддерживал его годами. Но вот незадача: не все команды и не все обёртки правильно обрабатывают --.
Вот типичный пример, который приводится в статье Nesbitt:
# Плохо: уязвимо к инъекции
GIT_COMMIT_MSG=$(curl https://api.example.com/last-commit-msg)
git commit -m "$GIT_COMMIT_MSG"
Если API вернёт --allow-empty-message -m '', то Git выполнит git commit --allow-empty-message -m '', что создаст пустой коммит — возможно, не то, что вы хотели. Но если злоумышленник подставит что-то вроде --exec=cat /etc/passwd, последствия могут быть куда серьёзнее.
Почему git commit -- "$GIT_COMMIT_MSG" не спасает? Потому что -m — это опция, а не позиционный аргумент. Старый -- отделяет только файлы и пути, но не влияет на разбор опций, которые уже встретились до него.
Как появился --end-of-options?
В Git 2.47 (выпущен в конце 2024 года) появился новый разделитель: --end-of-options. Он работает как глобальный стоп-сигнал: после него Git перестаёт интерпретировать любые строки как флаги, даже если они начинаются с тире.
В июле 2026 года разработчики выпустили патч, который исправляет давнюю ошибку: теперь многие команды Git (включая git commit, git log, git diff) корректно обрабатывают --end-of-options даже в тех сценариях, где раньше игнорировали его. Это закрывает целый класс уязвимостей.
Вот как теперь должен выглядеть безопасный скрипт:
# Хорошо: защита от инъекции
GIT_COMMIT_MSG=$(curl https://api.example.com/last-commit-msg)
git commit --end-of-options -m "$GIT_COMMIT_MSG"
Но внимание: флаг должен стоять строго перед опциями, которые могут прийти из недоверенного источника. Если вы напишете git commit -m --end-of-options "$MSG", Git сначала обработает -m как опцию, а уже потом наткнётся на --end-of-options — и тогда "$MSG" будет воспринят как значение для -m, а не как отдельный аргумент. То есть порядок имеет значение.
Практическое руководство: как защитить свои скрипты
1. Обновите Git до версии 2.47+
Проверьте текущую версию:
git --version
Если у вас 2.46 или старше — --end-of-options может не работать или работать некорректно. Скачать последнюю версию можно с официального сайта Git или через ваш пакетный менеджер.
2. Используйте --end-of-options во всех командах, принимающих пользовательский ввод
Список команд, которые поддерживают флаг (по данным релиз-ноутов):
| Команда | Пример использования |
|---|---|
git commit |
git commit --end-of-options -m "$msg" |
git log |
git log --end-of-options -- "$path" |
git diff |
git diff --end-of-options "$commit1" "$commit2" |
git show |
git show --end-of-options "$ref" |
git tag |
git tag --end-of-options -a "$tagname" -m "$msg" |
git branch |
git branch --end-of-options "$branchname" |
3. Экранируйте аргументы в CI/CD
Если вы используете GitHub Actions, GitLab CI или Jenkins, добавьте --end-of-options в шаги, где вызывается Git с динамическими параметрами. Например, в GitHub Actions:
- name: Create commit
run: |
git commit --end-of-options -m "${{ github.event.head_commit.message }}"
4. Проверьте хуки
Pre-commit, commit-msg и post-commit хуки часто принимают аргументы от Git. Если вы передаёте их дальше в другие команды, оберните в --end-of-options:
#!/bin/sh
# .git/hooks/prepare-commit-msg
git commit --end-of-options --allow-empty -m "$1"
5. Комбинируйте с -- для путей
Если вы передаёте и опции, и пути, ставьте оба разделителя:
git diff --end-of-options "$commit" -- "$file"
Здесь --end-of-options защищает опции, а -- отделяет пути.
Почему это важно прямо сейчас?
В июле 2026 года исследователи безопасности опубликовали PoC-эксплойт, который использует отсутствие --end-of-options в старых версиях Git для выполнения произвольных команд через webhook-интеграции. Проблема затронула многие открытые репозитории, которые автоматически обрабатывают коммиты от внешних контрибьюторов.
Суть атаки: злоумышленник создаёт коммит с сообщением, содержащим --exec=malicious.sh. Если серверный скрипт запускает git log --oneline $commit_hash, то Git воспринимает --exec=malicious.sh как опцию и выполняет скрипт. C --end-of-options такая атака становится невозможной.
Что делать, если ваша версия Git не поддерживает флаг?
Если по каким-то причинам вы не можете обновиться, есть обходные пути:
- Фильтрация ввода: удалите из пользовательского ввода все строки, начинающиеся с тире, если они не ожидаются.
- Использование
printf: передавайте аргументы через stdin, а не через аргументы командной строки. - Обёртки на Python/Perl: используйте библиотеки, которые экранируют аргументы (например,
shlex.quote()в Python).
Но лучший способ — обновить Git.
Заключение
Флаг --end-of-options — не просто удобная фича, а необходимый элемент безопасности для любого скрипта, который передаёт Git данные из ненадёжных источников. Июльский патч 2026 года закрыл критическую брешь, которая существовала годами. Если вы до сих пор не используете этот флаг в своих CI-пайплайнах, хуках или автоматизированных сценариях — сейчас самое время начать.
Обновите Git, добавьте --end-of-options в критически важные команды, и ваши репозитории скажут вам спасибо.
Эта статья основана на техническом анализе от Nesbitt и официальной документации Git. Подробности — в первоисточнике.
Комментарии