EnglishРусский Map
Supply Chain Security

Извлечение токенов из памяти CI-runner'а

title
Извлечение токенов из памяти CI-runner'а
type
concept
summary
Код на CI-runner'е читает память процесса worker'а для извлечения OIDC-токенов и прямой публикации в реестр
tags
security, github-actions, ci-cd, oidc
created
2026-05-12
updated
2026-05-12
lang
ru
source_updated
2026-05-12
translated
2026-09-01
translator
lllm/antigravity/gemini-3.7-flash-medium

Класс атак, при котором вредоносный код, получивший возможность исполнения на CI-runner'е, напрямую читает память процесса хоста runner'а для извлечения OIDC-токена - а затем использует этот токен для аутентификации во внешнем сервисе (npm, PyPI, AWS через федеративную идентификацию) в обход объявленных в workflow шагов публикации.

Это то самое связующее звено, которое превращает "исполнение кода на runner'е" в "запись в продакшен-реестр пакетов".

Как это работает (специфика GitHub Actions)

На хостящихся у GitHub runner'ах (GitHub-hosted runners) работает процесс Runner.Worker, который отвечает за выполнение workflow. Когда в workflow объявлено id-token: write, а какой-либо шаг запрашивает OIDC-токен (getIDToken() в @actions/core, неявный вызов внутри шагов с поддержкой Trusted Publishing и т. д.), runner лениво выпускает JWT и держит его в памяти.

Если код атакующего оказывается на той же машине - и работает под тем же пользователем, что и worker (а по умолчанию так и есть), - он может:

  1. Найти PID процесса Runner.Worker через /proc/*/cmdline
  2. Прочитать /proc/<pid>/maps, чтобы узнать структуру адресного пространства worker'а
  3. Прочитать /proc/<pid>/mem, чтобы сделать дамп этой памяти
  4. Найти JWT по шаблону (префикс base64 eyJ... служит быстрым ориентиром)
  5. Отправить JWT POST-запросом на endpoint доверенной OIDC-публикации (trusted publisher) реестра

Токен остаётся валидным до истечения срока действия - обычно несколько минут - и любая публикация в течение этого окна неотличима от публикации, сделанной легитимным шагом Publish Packages. Реестр видит запрос на публикацию от workflow с валидной привязкой и не имеет возможности проверить происхождение запроса на уровне конкретного шага.

Один и тот же скрипт для дампа памяти (буквально один в один, включая комментарий с указанием авторства) был замечен в:

  • Компрометации tj-actions/changed-files, март 2025 - первоначальная публикация через force-push тега в action без фиксации версии.
  • Компрометации TanStack @tanstack/*, май 2026 - см. tanstack-npm-supply-chain-postmortem.

Авторы скрипта называют эту технику тем, [от чего реестр защитить не может], - и они правы. Защита должна быть на стороне workflow.

Почему шаг Publish Packages в workflow не имеет значения

Распространённое ложное чувство безопасности: "Для шага публикации у нас настроено ручное подтверждение / отдельное окружение / подпись аппаратным ключом". Всё это не имеет значения, если OIDC-токен можно выпустить из любого job'а в workflow с id-token: write. Атакующему не нужно проникать в ваш шаг публикации. Он выпускает токен сам и шлёт POST-запрос напрямую в реестр.

Инцидент с TanStack - наглядный пример. Легитимный шаг Publish Packages был пропущен, поскольку упали тесты. Но публикации всё равно произошли - из фазы тестирования и очистки, где бинарники атакующего выполнились после восстановления отравленного кэша.

Способы защиты

Надёжное решение - разделение исполнения кода и владения токеном.

Перенести публикацию в job, не выполняющий код участников проекта.

  • Типичная безопасная схема: job 1 собирает проект и создаёт артефакт (загружаемый через actions/upload-artifact). Job 2 имеет id-token: write, не запускает сторонних зависимостей, скачивает артефакт, сверяет его с манифестом хэшей, подписанным в другом месте, и публикует. Если в окружении job 2 нет ничего, что могло бы исполнить код атакующего, добраться до токена в памяти невозможно.

Ограничить id-token: write только одним job'ом.

  • Задайте permissions: {} на уровне workflow и добавляйте id-token: write только в job публикации. Принцип "запрещено по умолчанию" для каждого job'а.

Не восстанавливать кэши в job'е публикации.

  • Job публикации не должен вызывать actions/cache@v... для путей, в которые мог записать данные код атакующего. Загружайте зависимости заново в этом job'е или восстанавливайте их из изолированной области кэша (cache scope), в которую не мог писать запуск из PR. Это разрушает связку github-actions-cache-poisoning -> извлечение токена.

Фиксировать все action'ы по SHA коммита.

  • Плавающий тег @v... у стороннего action'а сам по себе служит вектором извлечения (как в случае с tj-actions/changed-files). Инструмент pinact и режим pinned в Dependabot автоматизируют этот процесс.

Проверка источника происхождения (на стороне реестра, в масштабе экосистемы).

  • Структурное решение - научить реестры фиксировать путь к файлу workflow и ID шага, выполнившего публикацию, и дать авторам пакетов возможность указывать, какому именно шагу разрешено запрашивать токен. Это обсуждается, но пока не внедрено. До тех пор защита целиком держится на мерах со стороны издателя пакета, описанных выше.

См. также