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