Статья Атаки на supply chain GitHub: username hijacking, угон репозиториев и защита CI/CD

ff4f9445-2dd0-4c01-aabd-9be440675cca.webp


На последнем аудите 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 - если подмодуль ссылается на переименованный аккаунт
Ключевая проблема: в большинстве организаций никто не мониторит, жив ли namespace, на который ссылается зависимость. Её добавили два года назад, автор переименовал аккаунт - и вот ваш пайплайн тянет код из пустоты, которую может заполнить любой желающий.

Реальные инциденты: как 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:
  1. Начальная точка - SpotBugs. Атакующий получил доступ к CI/CD-токену через SpotBugs-репозиторий.
  2. Прыжок к reviewdog. С помощью украденного токена скомпрометирован workflow в reviewdog.
  3. Компрометация tj-actions. Через pull_request_target эксплойт в reviewdog атакующий извлёк PAT с правами на tj-actions/changed-files.
  4. Перезапись тегов. Атакующий перезаписал Git-теги tj-actions/changed-files, указав их на коммиты с вредоносным кодом.
  5. Массовое распространение. Каждый репозиторий с uses: tj-actions/changed-files@v35 (или другим тегом) при следующем запуске CI получал вредоносный payload.
Payload дампил секреты из окружения CI/CD runner'а и записывал их прямо в лог workflow. По данным Unit 42, изначально целью была Coinbase, но потом атака расползлась на все репозитории, использующие этот action.

Вредоносные коммиты делались от специально созданных аккаунтов - 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
Также была создана поддельная версия бинарника trivy - 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  # Выполнение кода атакующего с доступом к секретам
Атакующий создаёт PR с модифицированным 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
Шаг 2: Проверка существования namespace
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
Шаг 3: Проверка Git-зависимостей в package.json
Bash:
# Поиск Git-ссылок на GitHub в зависимостях
grep -r "github:" node_modules/*/package.json 2>/dev/null | grep -oP 'github:\K[^#"]+' | sort -u
Каждый результат со статусом 404 - потенциальный вектор для username hijacking. Namespace свободен - атакующий регистрирует аккаунт с этим именем и создаёт репозиторий-ловушку. На одном аудите я нашёл так три «мёртвых» namespace'а - все вели к actions, которые запускались на каждый push в main.

Для 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
Комментарий с версией - для читаемости. Реальная привязка - SHA.

Для автоматизации есть pin-github-action:
Bash:
npm install -g pin-github-action
pin-github-action .github/workflows/ci.yml
Митигация 2: Запрет pull_request_target с checkout PR-кода

Добавьте в .github/CODEOWNERS защиту workflow-файлов:
Код:
# .github/CODEOWNERS
.github/workflows/ @security-team
Включите branch protection rule с обязательным review от @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
Митигация 3: Ограничение прав GITHUB_TOKEN

По умолчанию GITHUB_TOKEN имеет широкие права. Режьте их на уровне workflow:
YAML:
permissions:
  contents: read    # Только чтение кода
  packages: none    # Нет доступа к пакетам
  actions: none     # Нет доступа к управлению actions
На уровне организации - установите default permissions в 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
По данным Snyk, в инциденте с Trivy использовался fallback-механизм эксфильтрации - если прямое подключение блокировалось, payload пытался отправить данные через DNS-туннелирование. Так что мониторинг аномальных DNS-запросов из runner'а - ещё один необходимый слой.

Митигация 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
Scorecard покажет, используются ли branch protection rules, подписаны ли релизы, ограничены ли права токенов. Не панацея, но хотя бы видно, во что ввязываешься.

Митигация 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
Snyk рекомендует пять немедленных шагов при подозрении на компрометацию:
  1. Прекратить использование скомпрометированного action по тегу
  2. Ротировать все credentials, доступные из CI/CD
  3. Проверить наличие персистентности атакующего (новые SSH-ключи, webhook'и)
  4. Проверить fallback-механизмы эксфильтрации (аномальный сетевой трафик)
  5. Запустить сканер компрометации (если доступен от вендора)

Более широкая картина: 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 - у вас та же проблема, что была у меня.
 
Последнее редактирование:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab