Два CVE с CVSS 10.0 - потолок шкалы NVD. Ноль привилегий у атакующего, ноль взаимодействия со стороны maintainer'а. На выходе - Docker Hub credentials, Personal Access Token и SSH-ключ подписи коммитов. Всё, что понадобилось: анонимный GitHub-аккаунт, форк публичного репозитория CloudPirates Open Source Helm Charts и один pull request.
Обе уязвимости закрыты коммитом
fcf9302. Но паттерн, который их породил, живёт в тысячах репозиториев. И находится за 30 секунд через GitHub Code Search - я проверял.Бизнес-логика атаки: зачем атакующему CI/CD-пайплайн Helm Charts
[Применимо: внешний пентест / bug bounty, публичные репозитории GitHub с CI/CD-пайплайнами] Подробнее - в нашем обзоре атаки на цепочку поставок.CloudPirates Open Source Helm Charts - коллекция чартов для Kubernetes-деплоя. Казалось бы, ну чарты и чарты. Но компрометация CI/CD этого репозитория давала атакующему три направления:
Перехваченные Docker Hub credentials позволяют выполнить
docker login, собрать образ с бэкдором и запушить его под легитимным тегом. Каждый helm install или docker pull у downstream-пользователей доставляет вредоносный код. По MITRE ATT&CK - Malicious Image (T1204.003, Execution).SSH-ключ подписи и PAT дают возможность коммитить в основную ветку от имени maintainer'а. Изменённые Helm-шаблоны могут встроить вредоносные
initContainers в деплойменты - Compromise Software Dependencies and Development Tools (T1195.001, Initial Access). GitHub покажет зелёный бейдж Verified, и никто не почешется.Personal Access Token GitHub обычно имеет доступ к нескольким репозиториям организации. Один PR - и атакующий получает плацдарм для перемещения по всей DevOps-инфраструктуре: Steal Application Access Token (T1528, Credential Access).
Это не теория. По данным Orca Security, исследователи скомпрометировали репозитории Microsoft, Google и NVIDIA - каждый раз через единственный fork pull request. Sysdig TRT нашёл десятки уязвимых workflow в проектах MITRE ATT&CK CAR и Splunk Security Content. А CVE-2025-30066 (tj-actions/changed-files, CVSS 8.6, CWE-506) попал в CISA KEV с подтверждённой эксплуатацией в дикой природе - затронуто свыше 23 000 репозиториев.
pull_request vs pull_request_target: корень уязвимости секретов CI/CD
Вся проблема - в том, как GitHub Actions обрабатывает два похожих триггера. Разница между ними не нюанс конфигурации, а граница безопасности. И эту границу пересекают с завидной регулярностью.| Характеристика | pull_request | pull_request_target |
|---|---|---|
| Контекст выполнения | merge commit (код PR) | base branch (основная ветка) |
| Доступ к секретам репозитория | Нет (для fork PR) | Да |
| Права GITHUB_TOKEN | read-only | read/write по умолчанию |
| Запуск от fork'ов | Требует approval | Запускается автоматически |
| Целевое назначение | Тестирование кода PR | Автоматизация с привилегиями |
Триггер
pull_request_target задумывался для задач, не требующих выполнения кода из PR: проставить лейбл, оставить комментарий, обновить статус чека. Его привилегированный контекст - фича, а не баг. Проблема возникает, когда workflow с pull_request_target делает actions/checkout с ссылкой на head.ref или head.sha из PR - тут-то и начинается: код атакующего выполняется в привилегированном окружении.GitHub предупредил об этом ещё в августе 2021 года. Толку мало. По данным GitHub Security Lab, разработчики регулярно совершают одну из трёх ошибок:
Прямой checkout PR-кода с параметром
ref: ${{ github.event.pull_request.head.ref }} - классический путь к code injection. Workflow скачивает код из форка, но выполняет его с секретами основного репозитория. Красота.TOCTOU-атака (Time of Check, Time of Use) - PR проходит ревью, получает одобрение, после чего атакующий дописывает вредоносный коммит до запуска workflow. Ссылка на
head.ref (ветку) мутабельна - атакующий может в любой момент обновить её содержимое. Ссылка на конкретный head.sha (коммит) иммутабельна, но и она не спасает, если workflow запускается автоматически при synchronize.Отравление кэша - даже workflow с
permissions: {} может записать вредоносные данные в Actions cache. Другие, привилегированные workflow, которые используют тот же кэш, выполнят подменённый код. Самый подлый вариант - не оставляет следов в конфигурации привилегированного workflow.CVE-2026-45131 и CVE-2026-45132: два workflow, один паттерн
Оба CVE затрагивают CloudPirates Open Source Helm Charts. Оба получили CVSS 10.0 (CRITICAL), оба классифицированы как CWE-94 (Improper Control of Generation of Code), оба исправлены одним коммитом
fcf9302. Но компрометируют разные workflow с разными последствиями.pull-request.yaml: Docker Hub credentials без одобрения maintainer'а
CVE-2026-45131 - workflowpull-request.yaml выполнял код из fork pull request в привилегированном контексте, раскрывая repository secrets: Docker Hub credentials и токены. Одобрение maintainer'а не требовалось.Вектор CVSS:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N. Разберём по компонентам:- AV:N - атака через сеть (веб-интерфейс GitHub или API)
- AC:L - никаких специальных условий, достаточно создать PR
- PR:N - анонимный GitHub-аккаунт, ноль привилегий в целевом репозитории
- UI:N - maintainer не должен ничего нажимать
- S:C (Changed scope) - компрометация выходит за границы репозитория: Docker Hub, downstream-пользователи образов
- C:H / I:H - полный доступ к секретам и возможность модификации (push образов, изменение кода)
total, Exploitation как none (на дату анализа активной эксплуатации не зафиксировано), но Automatable: yes - атака полностью автоматизируема скриптом.generate-schema.yaml: PAT и SSH-ключ подписи коммитов
CVE-2026-45132 затрагивает другой workflow -generate-schema.yaml, который раскрывал Personal Access Token и SSH signing key для fork-контролируемого кода из-за небезопасного checkout'а и обработки credentials.CVSS-вектор идентичен: 10.0, тот же CWE-94. Но последствия шире. SSH signing key позволяет подписывать коммиты от имени maintainer'а - GitHub покажет зелёный бейдж
Verified. А PAT обычно имеет более широкий scope, чем GITHUB_TOKEN, включая доступ к приватным репозиториям организации. Если организация не ограничивала scope PAT по принципу минимальных привилегий (а кто ограничивает?) - один скомпрометированный токен открывает доступ ко всей кодовой базе.Полная цепочка атаки: от форка до supply chain compromise
[Применимо: внешний пентест / bug bounty против публичных GitHub-репозиториев с CI/CD. Не применимо к приватным репозиториям без fork-доступа.]
Требования к окружению для воспроизведения: GitHub-аккаунт (бесплатный), браузер или
gh CLI (v2.x+), для локального анализа workflow - act (локальный раннер GitHub Actions, активный open-source проект). ОС: любая с доступом к GitHub. Для поиска уязвимых репозиториев - GitHub Code Search (встроенный, не требует ничего дополнительного).Шаг 1. Разведка - Code Repositories (T1213.003, Collection). Атакующий находит публичный репозиторий с
pull_request_target в .github/workflows/. Поиск тривиален: GitHub Code Search по запросу pull_request_target path:.github/workflows filename:*.yml в комбинации с actions/checkout и head.ref. Sysdig TRT использовал именно этот подход и нашёл уязвимые workflow в репозиториях MITRE, Splunk и десятках других проектов.Шаг 2. Подготовка payload - Poisoned Pipeline Execution (T1677, Execution). Атакующий форкает репозиторий и модифицирует файл, который workflow исполняет. Вариантов несколько: подмена shell-скрипта из
run: блока, модификация setup.py (при pip install . выполняется произвольный Python-код), изменение Makefile или package.json. В случае CVE-2025-47928 (spotipy, CVSS 9.1, CWE-488) исследователи Sysdig TRT модифицировали setup.py, добавив кастомный install-хук, который дампил память процесса runner worker через публично доступный скрипт memdump.py и отправлял секреты на внешний S3-бакет. Открытие PR на основной репозиторий триггерит workflow автоматически - Unix Shell (T1059.004, Execution).Шаг 3. Эксфильтрация секретов - Credentials In Files (T1552.001, Credential Access). Payload обращается к переменным окружения:
$DOCKER_PASSWORD, $DOCKER_USER, $GITHUB_TOKEN, или дампит память runner worker. Секреты отправляются на контролируемый сервер - Exfiltration Over Webhook (T1567.004, Exfiltration). Для CVE-2025-47928 были успешно извлечены SPOTIPY_CLIENT_ID, SPOTIPY_CLIENT_SECRET и GITHUB_TOKEN с дефолтными write-правами на весь репозиторий.Шаг 4. Post-exploitation. С Docker Hub credentials атакующий пушит образ с бэкдором под легитимным тегом. С PAT - коммитит в приватные репозитории организации. С SSH signing key - подписывает коммиты с verified-бейджем. Всё чисто, всё "легитимно".
Уязвимый паттерн, общий для обоих CVE и десятков аналогичных находок:
YAML:
on:
pull_request_target:
types: [opened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- run: ./scripts/build.sh
ref и repository указывают на форк атакующего. После checkout'а ./scripts/build.sh - уже не оригинальный скрипт, а заменённый атакующим. Но выполняется он с секретами основного репозитория. Вот и весь фокус.Ограничения техники. Require approval for all outside collaborators (Settings -> Actions -> Fork pull request workflows) замедляет атаку, но не блокирует - TOCTOU: PR одобряют, атакующий дописывает вредоносный коммит, workflow берёт обновлённый
head.ref. Branch protection rules на pull_request_target не влияют - workflow запускается до merge. Приватные репозитории без fork-доступа к этому вектору неуязвимы. Организации с OIDC-аутентификацией вместо long-lived Docker Hub credentials снижают импакт: скомпрометированный OIDC-токен ограничен scope'ом и временем жизни.Чеклист аудита GitHub Actions workflow для пентестера
Готовый список для передачи в отчёт или проверки на bug bounty. Копируйте как есть.Поиск уязвимых workflow:
- Клонируйте целевой репозиторий, выполните
grep -r "pull_request_target" .github/workflows/ - Для каждого найденного файла проверьте
actions/checkoutсref:, указывающим наhead.ref,head.shaили merge ref PR - Проверьте, выполняет ли workflow код из чекаута:
run:блоки,pip install .,npm install,make, вызовы скриптов из репозитория - Проверьте блок
permissions:- его отсутствие означает дефолтные read/write права дляGITHUB_TOKEN - Ищите
secrets.*вenv:илиwith:блоках - они доступны исполняемому коду - Проверьте
workflow_runтриггеры, запускающиеся послеpull_request- потенциальный privilege escalation: непривилегированный workflow триггерит привилегированный
- Замените
pull_request_targetнаpull_requestвезде, где workflow не требует доступа к секретам - Если
pull_request_targetнеобходим - добавьте проверку источника:if: github.event.pull_request.head.repo.full_name == github.repository- это отсекает fork PR - Используйте workflow splitting: непривилегированный
pull_requestвыполняет тесты, привилегированныйworkflow_runобрабатывает результаты, но с жёсткой валидацией артефактов на границе - Задайте минимальные
permissionsв каждом workflow явно -contents: readвместо дефолтных - Переведите Docker Hub и облачных провайдеров на OIDC вместо long-lived credentials в секретах
- На уровне организации: Settings -> Actions -> Fork pull request workflows -> Require approval for all outside collaborators
- Автоматизируйте аудит:
actionlint(статический анализ workflow, активный проект) иsemgrepс правилами для YAML-конфигов Actions выявляют описанные паттерны до merge
EPSS vs CVSS: приоритизация уязвимостей CI/CD на пентесте
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Паттерн не меняется: checkout PR-кода в привилегированном контексте. Проблема не в сложности атаки - она тривиальна для любого, кто прочитал полстраницы документации GitHub. Проблема в дефолтном поведении:
pull_request_target из коробки имеет write-доступ к GITHUB_TOKEN и полный доступ к секретам. Пока дефолт не станет secure-by-default - read-only, без секретов, с обязательным approval - каждый новый публичный репозиторий с CI/CD-пайплайном это потенциальный CVE 10.0.Аргумент "у нас маленький проект, нас никто не атакует" разбивается об автоматизацию: GhostAction скомпрометировал 817 репозиториев одним скриптом, не выбирая по размеру. На каждом внешнем пентесте или bug bounty, где в scope есть GitHub-организация, первое, что я проверяю -
.github/workflows/*.yml на pull_request_target с unsafe checkout. Это 30 секунд работы. И с ненулевой вероятностью - critical finding.Пока GitHub не изменит дефолт, этот вектор останется самым дешёвым entry point в supply chain. Попробуйте прогнать свои репозитории через
grep -r "pull_request_target" .github/workflows/ прямо сейчас - результат может удивить. На WAPT эту цепочку разбирают в модуле по CI/CD с полноценной лабой.
Последнее редактирование модератором: