Новости

Typosquat-пакеты npm крадут секреты CI: что проверить до следующего деплоя

Дмитрий Потоцкий 2 мин чтения
Typosquat-пакеты npm крадут секреты CI: что проверить до следующего деплоя

28 мая 2026 года один автор загрузил в npm 14 вредоносных пакетов за четыре часа. Названия походили на библиотеки OpenSearch, Elasticsearch и DevOps-инструменты. Некоторые пакеты указывали официальный репозиторий OpenSearch в package.json, чтобы не вызвать подозрений при беглом просмотре.

Если такой пакет попал на раннер, обычный npm install мог отдать атакующему AWS credentials, токены Vault, GitHub Actions и npm. До следующего деплоя тебе нужно найти следы запуска, отозвать доступные пакету секреты и отделить установку зависимостей от продакшен-доступа.

Вредоносный код запускался до сборки

Все 14 пакетов объявляли install-hook в package.json. npm запускал его автоматически при установке зависимости.

Microsoft обнаружила две цепочки:

node → preinstall.js → HTTP C2 → payload.bin
node → setup.mjs → download Bun → stage-2

В первом варианте preinstall.js отправлял сведения о хосте на командный сервер, скачивал payload и запускал его отдельным процессом. HTTP-запрос содержал заголовок X-Supply: 1.

Во втором варианте setup.mjs скачивал легитимный Bun, а затем запускал через него вторую стадию. Оба варианта использовали один Bun-бинарник размером около 195 КБ.

Install-hook получал те же переменные окружения и права, что и шаг установки. Если job уже экспортировал AWS_ACCESS_KEY_ID, VAULT_TOKEN или NPM_TOKEN, отдельный взлом контейнера не требовался. CI/CD-шаги часто работают под привилегированными учётными записями, поэтому одна зависимость получает доступ сразу к нескольким системам.

Сначала проверь сетевые логи и историю раннеров

Начни с точных индикаторов этой кампании. Проверь DNS, proxy и firewall на обращения к домену:

aab.sportsontheweb[.]net

Microsoft рекомендует заблокировать этот домен на DNS-, proxy- и firewall-уровнях. Для HTTP-трафика добавь поиск и алерт по заголовку:

X-Supply: 1

Оба индикатора относятся к описанной кампании, а не ко всем вредоносным npm-пакетам.

Затем ищи процессы и файлы, связанные с обеими цепочками:

preinstall.js
setup.mjs
payload.bin
bun

Само имя bun ещё не доказывает заражение. Сопоставь его с временем выполнения npm install, неожиданной загрузкой runtime и исходящим соединением раннера. Цепочка событий сильнее одиночного совпадения.

Если логи запускает systemd, сузь поиск интервалом подозрительной сборки:

journalctl -u actions.runner.service \
 --since "2026-05-28 00:00:00" \
 --until "2026-05-29 00:00:00" |
 rg 'preinstall\.js|setup\.mjs|payload\.bin|bun|X-Supply'

Для Kubernetes-раннеров проверь логи pod и сетевой телеметрии за тот же интервал. В эфемерном раннере файлов уже может не быть, но DNS, proxy и audit logs сохранят цепочку.

Совпадение IOC означает ротацию токенов

Не жди подтверждения, что атакующий успел воспользоваться секретом. Если вредоносный hook работал в окружении с токеном, считай токен скомпрометированным.

Payload читал VAULT_TOKEN и VAULT_AUTH_TOKEN, проверял npm-токены через /-/whoami и запрашивал права публикации через /-/npm/v1/tokens. Украденный publish-токен позволял атакующему заражать следующие пакеты уже от имени доверенного владельца.

Для AWS вредоносный код обращался к EC2 Instance Metadata Service v2 по адресу 169.254.169.254 и перечислял секреты AWS Secrets Manager более чем в 16 регионах. Проверить только основной регион недостаточно.

При совпадении IOC:

  • останови job и запрети повторный запуск из того же workflow;
  • отзови AWS credentials и проверь CloudTrail;
  • отзови токены Vault и просмотри audit log;
  • удали GitHub-токены, доступные job;
  • отзови npm-токены и проверь историю публикаций пакетов.

Ищи не только успешные действия. Запрос whoami, отказ в доступе к Secrets Manager или неудачная публикация пакета тоже показывают, что payload добрался до учётных данных.

OIDC сокращает срок жизни украденного доступа

Статический AWS-ключ в переменной окружения переживает job, раннер и иногда сотрудника, который его создал. OIDC federation заменяет такой ключ короткоживущим токеном, выданным конкретному workflow.

Ограничь доверие не только репозиторием. Провайдер должен проверять branch, environment и identity workflow. Иначе любой job из разрешённого репозитория сможет запросить облачную роль.

OIDC не защищает секрет в момент выполнения job. Вредоносный install-hook всё ещё может украсть текущий токен. Разница в последствиях: срок действия ограничен, а следующий запуск не получит тот же credential.

Пароли баз и API-ключи сервисов без OIDC храни в secrets manager. Не записывай их в репозиторий, конфигурацию пайплайна, вывод консоли и shell history. Выдавай секрет после установки зависимостей и только шагу, которому он нужен.

Такой порядок безопаснее:

checkout → install → test → request short-lived token → deploy

Этот — опаснее:

export all secrets → npm install → test → deploy

Установка зависимостей не должна видеть продакшен

Перенеси npm install на одноразовый непривилегированный раннер без cloud-, Vault- и registry-токенов. Для публичных репозиториев постоянный self-hosted runner стоит заменить эфемерным или JIT-раннером: следующий job не должен наследовать файлы и процессы предыдущего.

Не запускай build-шаги от root или администратора. Для Docker убери --privileged: OWASP отдельно рекомендует не выдавать этот режим pipeline-контейнерам.

Ограничение прав не остановит install-hook. Оно уменьшит его радиус: пакет не прочитает файлы другого пользователя, не подключится к доступному Docker socket и не изменит хост через привилегированный контейнер.

Закрепи сторонние GitHub Actions по полному commit SHA. Теги latest и v1 можно передвинуть на другой коммит без изменения workflow, поэтому они не фиксируют исполняемый код.

steps:
 - uses: actions/checkout@<full-commit-sha>

Для npm сохраняй lock-файл в репозитории и ставь зависимости через npm ci. Lock-файл помогает увидеть изменение дерева зависимостей на review, но не делает пакет безопасным: закреплённая вредоносная версия останется вредоносной.

SBOM показывает, что попало в сборку

Проверка исходников не отвечает на вопрос, какие версии зависимостей оказались в артефакте. Сгенерируй CycloneDX SBOM из рабочей директории:

syft dir:. -o cyclonedx-json > sbom.json

Syft умеет сканировать локальные директории и контейнерные образы, а также выдавать CycloneDX или SPDX.

Перед публикацией проверь SBOM через Grype:

grype sbom:sbom.json

Grype читает CycloneDX и SPDX напрямую. Такой скан найдёт известные уязвимости, но не обязан распознать свежий typosquat. Имя нового пакета, неожиданная версия или лишняя зависимость всё равно требуют review.

После сборки подпиши образ Cosign и сохрани SLSA provenance. SLSA provenance связывает артефакт с процессом сборки, а Sigstore использует OIDC и transparency log для подписи и проверки.

Перед деплоем проверяй подпись:

cosign verify \
 --key cosign.pub \
 myregistry.local/app:"${TAG}"

В Kubernetes Kyverno или OPA Gatekeeper могут отклонить неподписанный workload до его запуска. Подпись не очищает заражённый образ — она не даёт незаметно заменить уже проверенный артефакт.

Что сделать до ближайшего деплоя

  1. Найди в DNS, proxy, firewall и логах раннеров домен aab.sportsontheweb[.]net, заголовок X-Supply: 1, файлы preinstall.js, setup.mjs, payload.bin и неожиданную загрузку Bun.
  2. При совпадении останови пайплайн и отзови AWS, Vault, GitHub и npm-токены. Проверь AWS Secrets Manager во всех доступных регионах и историю npm-публикаций.
  3. Заблокируй C2-домен на трёх уровнях: DNS, proxy и firewall. Добавь алерт на X-Supply: 1.
  4. Убери долгоживущие cloud credentials из environment variables. Выдавай job короткоживущий токен через OIDC только после установки и тестов.
  5. Запускай install-скрипты на одноразовом непривилегированном раннере без продакшен-секретов. Сторонние Actions закрепи по полному SHA.

Рабочее правило простое: install-скрипт зависимости — недоверенный код. Если npm install видит все секреты пайплайна, typosquat превращается из испорченной сборки в доступ к облаку, Vault и registry.