На последнем аудите CI/CD - организация с монорепозиторием на 200+ микросервисов - я нашёл в workflow-файлах 14 ссылок на GitHub Actions, чьи namespace'ы принадлежали удалённым или переименованным аккаунтам. Любой мог зарегистрировать эти имена и получить выполнение произвольного кода внутри пайплайна. С доступом ко всем секретам. Никаких zero-day, никакой хитрой эксплуатации. Просто мёртвая ссылка в YAML-файле.
Атаки на supply chain GitHub - не абстрактная страшилка из отчётов. За последний год мы видели компрометацию
tj-actions/changed-files, поразившую тысячи репозиториев, и атаку на Trivy GitHub Actions, превратившую сканер безопасности в инструмент кражи credentials. Обе атаки эксплуатировали одно и то же - слепое доверие к сторонним зависимостям в CI/CD-пайплайнах. Разбираю конкретные механики - от username hijacking до pull_request_target - и даю проверяемые митигации с примерами конфигов.Что такое GitHub username hijacking и почему это проблема для CI/CD
GitHub username hijacking - захват освободившегося namespace после того, как пользователь переименовал или удалил аккаунт. Механика элементарна: пользовательalice переименовывается в alice-new, имя alice становится доступным. Кто угодно регистрирует alice, создаёт репозиторий с тем же именем - и все ссылки вида alice/some-action@v1 в чужих workflow-файлах теперь ведут на подконтрольный атакующему код.Это не теория. В хозяйстве GitHub Actions десятки тысяч workflow-файлов ссылаются на действия по имени пользователя и репозитория. Формат
uses: owner/repo@ref - фактически ссылка на произвольный код, который выполнится на вашем CI/CD runner'е с доступом к GITHUB_TOKEN и всем явно переданным секретам. Просто вдумайтесь: вы даёте незнакомцу запустить что угодно в своём конвейере.GitHub прикрутил механизм «popular repository namespace retirement» - если репозиторий был достаточно популярен (определённое число клонов перед переименованием), namespace блокируется. Но порог не публичен, защита покрывает не все случаи, и малоизвестные, но реально используемые в CI/CD actions часто остаются голыми.
Что именно оказывается под ударом
Угон репозитория GitHub через username hijacking бьёт по нескольким точкам:- GitHub Actions -
uses: owner/action@v1в workflow-файлах. Самый критичный вектор: код выполняется с правами runner'а - Зависимости в
package.json/go.mod- ссылки видаgithub.com/owner/repoв Go-модулях или Git-URL в npm-зависимостях - Документация и скрипты -
curl https://raw.githubusercontent.com/owner/repo/main/install.sh | bashв README или Makefile - Git submodules - если подмодуль ссылается на переименованный аккаунт
Реальные инциденты: как supply chain attack на GitHub выглядит на практике
tj-actions/changed-files - каскадная компрометация через pull_request_target
По данным Unit 42 (Palo Alto Networks), инцидент сtj-actions/changed-files начался с компрометации другого action - reviewdog. Атакующие использовали уязвимость в обработке pull_request_target триггера, чтобы вытащить Personal Access Token (PAT) с правами на tj-actions организацию.Цепочка атаки - классическое латеральное перемещение, только через CI/CD:
- Начальная точка - SpotBugs. Атакующий получил доступ к CI/CD-токену через SpotBugs-репозиторий.
- Прыжок к reviewdog. С помощью украденного токена скомпрометирован workflow в
reviewdog. - Компрометация tj-actions. Через
pull_request_targetэксплойт вreviewdogатакующий извлёк PAT с правами наtj-actions/changed-files. - Перезапись тегов. Атакующий перезаписал Git-теги
tj-actions/changed-files, указав их на коммиты с вредоносным кодом. - Массовое распространение. Каждый репозиторий с
uses: tj-actions/changed-files@v35(или другим тегом) при следующем запуске CI получал вредоносный payload.
Вредоносные коммиты делались от специально созданных аккаунтов -
iLrmKCu86tjwp8, 2ft2dKo28UazTZ, mmvojwip, jurkaofavak. Имена говорят сами за себя - чистые throwaway.Trivy GitHub Actions - когда сканер безопасности становится бэкдором
По детальному анализу Snyk, атакующие скомпрометировали 75 версионных тегов популярного Trivy GitHub Action (aquasecurity/trivy-action). Инструмент безопасности превратился в средство кражи credentials. Ирония? Нет, закономерность.Атака развивалась в три стадии:
Стадия 1 (конец февраля): Эксплуатация
pull_request_target - атакующие создали pull request, который выполнялся в контексте целевого репозитория, и вытащили секреты.Стадия 2 (1 марта): Первая компрометация - получен доступ к токенам для публикации, вредоносный код внедрён в action.
Стадия 3 (19 марта): Масштабная компрометация - перезаписаны 75 тегов, payload стал самораспространяющимся.
Payload различался в зависимости от среды:
- На GitHub-hosted runners - дамп переменных окружения:
GITHUB_TOKEN,AWS_ACCESS_KEY_IDи прочие секреты - На self-hosted runners - дополнительно сбор SSH-ключей и конфигурации Docker
- На машинах разработчиков - сбор файлов из
~/.ssh,~/.aws,~/.docker
v0.69.4 - со встроенным бэкдором (по данным Snyk). Подробнее о том, как Trivy используется в реальных пайплайнах и чем он отличается от конкурентов, можно прочитать в материале про сравнение Trivy, Clair и Anchore.Оба инцидента - одна и та же атака на цепочку поставок ПО: компрометация upstream-зависимости каскадно бьёт по всем downstream-потребителям. Один action - тысячи жертв.
Dependency confusion и typosquatting: смежные векторы атак
Username hijacking - не единственный способ подмены пакетов. Рядом работают ещё два вектора, и их часто комбинируют.Dependency confusion атака
Dependency confusion эксплуатирует приоритет разрешения пакетов. Организация использует внутренний npm/PyPI registry с пакетом@company/utils. Атакующий публикует company-utils (или даже @company/utils, если scope не защищён) в публичный registry с более высокой версией. Пакетный менеджер, если не настроен корректно, подтянет публичную версию. Тупо по номеру версии.Typosquatting npm PyPI
Typosquatting - регистрация пакетов с именами, похожими на популярные:requets вместо requests, colorsss вместо colors. По данным Socket.dev, ежемесячно в npm и PyPI обнаруживаются сотни пакетов-тайпосквоттеров. Сотни - каждый месяц.В контексте CI/CD оба вектора опасны тем, что вредоносный пакет npm или Python-пакет выполняет lifecycle-скрипты (
postinstall в npm, setup.py в pip) с правами процесса сборки.Связь с username hijacking
Эти векторы пересекаются. Представьте: автор популярного npm-пакета использовал GitHub-ссылку вpackage.json → "dependencies": {"helper": "github:olduser/helper"}. Автор переименовал аккаунт. olduser - свободный namespace. Атакующий регистрирует olduser, создаёт helper с бэкдором - и все npm install у пользователей этого пакета подтянут вредоносный код. Красивая матрёшка, если подумать. Детальный разбор этого вектора применительно к serverless-окружениям - в статье про атаки через dependency confusion.Безопасность CI/CD pipeline: как pull_request_target становится точкой входа
Один из главных уроков из инцидентов с tj-actions и Trivy - триггерpull_request_target как вектор атаки. Лично я считаю его одним из самых недооценённых рисков в GitHub Actions.Почему pull_request_target опасен
Стандартный триггерpull_request запускает workflow из кода PR-ветки, но с ограниченными правами - без доступа к секретам репозитория. Триггер pull_request_target создавался для обхода этого ограничения: он запускает workflow из основной ветки (где есть секреты), но в контексте pull request.Проблема возникает, когда workflow с
pull_request_target делает checkout кода из PR:
YAML:
# ОПАСНЫЙ ПАТТЕРН - не используйте
on:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install && npm test # Выполнение кода атакующего с доступом к секретам
package.json (добавляет postinstall-скрипт) или меняет тестовые файлы - и получает выполнение произвольного кода с доступом ко всем секретам репозитория. Вот так просто.Именно этот паттерн, по данным Snyk и Unit 42, использовался в обоих крупных инцидентах.
Мутация тегов как вектор компрометации зависимостей
Вторая критическая проблема - Git-теги мутабельны. Когда вы пишетеuses: some-action@v3, это ссылка на тег v3, который можно перезаписать в любой момент. Один git tag -f v3 <malicious-commit> && git push --force --tags - и все пользователи при следующем запуске получат совсем другой код.Именно так атакующие раскатали вредоносный payload через tj-actions и Trivy - перезаписали существующие теги. Никакого взлома - просто
--force.Практика: находим уязвимые namespace и защищаемся
Атакующая перспектива: разведка мёртвых namespace
Для пентестеров - вот как выглядит поиск уязвимых зависимостей. Все действия - только на разрешённых scope (ваша организация или bug bounty программа).Шаг 1: Сбор всех external actions из workflow-файлов
Bash:
# Поиск всех uses: в .github/workflows/ по организации
# Требует GitHub CLI (gh) с правами на чтение репозиториев
gh api "/orgs/YOUR_ORG/repos?per_page=100" --paginate -q '.[].full_name' | while read repo; do
gh api "/repos/$repo/contents/.github/workflows" -q '.[].name' 2>/dev/null | while read wf; do
gh api "/repos/$repo/contents/.github/workflows/$wf" -q '.content' | base64 -d | grep -oP 'uses:\s+\K[^@\s]+'
done
done | sort -u > actions_used.txt
Bash:
# Проверяем, живы ли владельцы этих actions
cat actions_used.txt | cut -d'/' -f1 | sort -u | while read owner; do
status=$(curl -s -o /dev/null -w "%{http_code}" "https://github.com/$owner")
if [ "$status" = "404" ]; then
echo "VULNERABLE: $owner - namespace свободен!"
fi
done
Bash:
# Поиск Git-ссылок на GitHub в зависимостях
grep -r "github:" node_modules/*/package.json 2>/dev/null | grep -oP 'github:\K[^#"]+' | sort -u
Для Semgrep можно написать правило, которое ловит ссылки на actions без SHA-пиннинга:
YAML:
# .semgrep/github-actions-unpinned.yml
rules:
- id: unpinned-github-action
patterns:
- pattern-regex: 'uses:\s+[\w\-]+/[\w\-]+@v\d+'
message: |
GitHub Action привязан к мутабельному тегу, а не к SHA-коммиту.
Используйте полный SHA: uses: owner/action@<commit-sha>
severity: WARNING
languages: [yaml]
paths:
include:
- ".github/workflows/*.yml"
- ".github/workflows/*.yaml"
Защитная перспектива: hardening CI/CD pipeline
Митигация 1: Пиннинг actions к SHA-коммитамЕдинственная надёжная защита от мутации тегов. Теги можно перезаписать, SHA - нет:
YAML:
# Плохо - тег мутабелен
- uses: actions/checkout@v4
# Хорошо - SHA иммутабелен
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Для автоматизации есть
pin-github-action:
Bash:
npm install -g pin-github-action
pin-github-action .github/workflows/ci.yml
Добавьте в
.github/CODEOWNERS защиту workflow-файлов:
Код:
# .github/CODEOWNERS
.github/workflows/ @security-team
@security-team для изменений в .github/workflows/.Если
pull_request_target действительно нужен (бывает), изолируйте его:
YAML:
on:
pull_request_target:
types: [labeled] # Только после ручной метки от мейнтейнера
jobs:
safe-build:
if: contains(github.event.pull_request.labels.*.name, 'safe-to-test')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false # Не передавать GITHUB_TOKEN в checkout
По умолчанию
GITHUB_TOKEN имеет широкие права. Режьте их на уровне workflow:
YAML:
permissions:
contents: read # Только чтение кода
packages: none # Нет доступа к пакетам
actions: none # Нет доступа к управлению actions
read-only через Settings → Actions → General → Workflow permissions (рекомендация GitHub Well-Architected Framework).Митигация 4: Мониторинг outbound-соединений из CI/CD
Оба инцидента включали эксфильтрацию данных на внешние серверы. На self-hosted runners настройте egress-фильтрацию:
Bash:
# Пример iptables-правил для self-hosted runner
# Разрешаем только необходимые домены
iptables -A OUTPUT -d github.com -j ACCEPT
iptables -A OUTPUT -d registry.npmjs.org -j ACCEPT
iptables -A OUTPUT -d pypi.org -j ACCEPT
# Блокируем всё остальное
iptables -A OUTPUT -j DROP
Митигация 5: OpenSSF Scorecard для оценки зависимостей
Bash:
# Установка scorecard
go install github.com/ossf/scorecard/v5/cmd/scorecard@latest
# Проверка конкретного action перед использованием
scorecard --repo=github.com/tj-actions/changed-files
# Автоматическая проверка в CI - смотрим Branch-Protection и Token-Permissions
scorecard --repo=github.com/owner/action --checks Branch-Protection,Token-Permissions --format json
Митигация 6: Gitleaks для поиска утечек секретов
Даже если атакующий получил выполнение кода в вашем CI, утечка секретов через логи - самый частый способ их извлечения (именно так работал payload в tj-actions). Gitleaks + pre-commit hook ловят это:
YAML:
# .github/workflows/gitleaks.yml
name: Gitleaks
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@cb7149a9b57195b609c63e8518d2c6056677d2d0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Чек-лист: полная защита от атак на supply chain GitHub
| Мера | Защищает от | Приоритет |
|---|---|---|
| Пиннинг actions к SHA | Мутация тегов, username hijacking | Критический |
| CODEOWNERS на .github/workflows/ | Несанкционированное изменение pipeline | Критический |
permissions: read-all по умолчанию | Избыточные права GITHUB_TOKEN | Критический |
| Запрет/аудит pull_request_target | Инъекция кода через PR | Высокий |
| Egress-фильтрация на self-hosted runners | Эксфильтрация секретов | Высокий |
| OpenSSF Scorecard для зависимостей | Слабо защищённые upstream actions | Средний |
| Require signed commits на releases | Подмена релизов | Средний |
Lockfile (package-lock.json, poetry.lock) | Dependency confusion атака | Высокий |
Disable lifecycle scripts (--ignore-scripts) | Typosquatting npm PyPI | Средний |
| Регулярная ротация секретов | Последствия утечки | Высокий |
Защита строится послойно - от отключения lifecycle-скриптов пакетных менеджеров до непрерывного мониторинга. Ни одна мера сама по себе не спасёт. Подробнее о том, как подписание коммитов и артефактов защищает цепочку поставок кода, - в отдельном материале.
Индикаторы компрометации: что искать прямо сейчас
Если вы используете или использовалиtj-actions/changed-files или aquasecurity/trivy-action - проверяйте немедленно:
Bash:
# Поиск в workflow поддельной версии Trivy
grep -r "v0.69.4" .github/workflows/
# Проверка, какие SHA использовались в последних запусках
gh run list --repo YOUR_ORG/YOUR_REPO --limit 50 --json databaseId -q '.[].databaseId' | while read id; do
gh run view $id --repo YOUR_ORG/YOUR_REPO --log 2>/dev/null | grep -i "trivy\|tj-actions"
done
- Прекратить использование скомпрометированного action по тегу
- Ротировать все credentials, доступные из CI/CD
- Проверить наличие персистентности атакующего (новые SSH-ключи, webhook'и)
- Проверить fallback-механизмы эксфильтрации (аномальный сетевой трафик)
- Запустить сканер компрометации (если доступен от вендора)
Более широкая картина: software supply chain security
Инциденты с tj-actions и Trivy - не аномалии. Это системная проблема. Open-source построен на доверии: когда вы пишетеuses: action@v1, вы делегируете выполнение кода незнакомцу, который может продать, потерять или переименовать аккаунт в любой момент.
Ссылка скрыта от гостей
(Supply-chain Levels for Software Artifacts) от Google предлагает модель зрелости для защиты supply chain. На уровне SLSA 3 каждый артефакт должен иметь проверяемую провенацию - кто, когда и из какого исходника его собрал. Реальность: большинство GitHub Actions даже не подписаны.Для пентестеров: username hijacking - это low-hanging fruit с потенциально катастрофическим импактом. Один свободный namespace может дать RCE в сотнях пайплайнов. Включайте проверку зависимостей CI/CD в scope каждого аудита - скрипт из раздела выше занимает 5 минут.
Для security-инженеров: задача - минимизировать blast radius. SHA-пиннинг, минимальные права токенов, egress-фильтрация и CODEOWNERS - четыре столпа, которые должны стоять до того, как случится следующий инцидент. Вопрос не «если» - вопрос «когда». Прогоните скрипт проверки namespace'ов по своей организации прямо сейчас. Если найдёте хоть один 404 - у вас та же проблема, что была у меня.
Последнее редактирование: