14 июля 2026 года атакующие скомпрометировали релизный пайплайн двух репозиториев AsyncAPI и опубликовали пять вредоносных версий пакетов с суммарным весом свыше 2.9 млн загрузок в неделю. Разбираем этот инцидент отдельно от остальных атак 2026 года не потому, что он крупнее, а потому что механика принципиально другая — и показывает дыру, которую не закрывают ни npm v12, ни allowScripts, ни staged publishing.
Таймлайн
Заранее, апрель 2026 — исследователь Florence Njeri публикует proof-of-concept для уязвимости pwn request в workflow-файле asyncapi/generator, который использовал триггер pull_request_target. Fix остаётся открытым и неслитым к моменту атаки.
14 июля, утро — атакующие организуют «спам-шторм» из пул-реквестов на репозиторий generator, чтобы забить CI/CD билдами и истощить внимание мейнтейнеров на триаже. Одновременно с шумом они закрывают уже поданный целевой вредоносный PR — маскируя его среди спама.
14 июля, 07:10 UTC — три пакета из монорепо @asyncapi/generator публикуются в npm: @asyncapi/generator@3.3.1, @asyncapi/generator-helpers@1.1.1, @asyncapi/generator-components@0.7.1. Публикация проходит через легитимный GitHub Actions release workflow самого проекта — с валидными npm OIDC provenance-аттестациями, потому что атакующие не украли npm-токен, а получили push-доступ к ветке next и позволили настоящему CI сделать публикацию за них.
Тем же днём — независимо скомпрометирован asyncapi/spec-json-schemas (репозиторий, из которого публикуется @asyncapi/specs) через тот же паттерн, но другой веткой (alpha). StepSecurity отмечает, что это два раздельных, параллельных компрометации, не одна цепочка.
11:18 UTC — все пять вредоносных версий сняты с публикации в npm. Актуальные dist-tag указывают на чистые версии (generator@3.3.0, generator-helpers@1.1.0, generator-components@1.0.0, specs@6.11.1). От первой публикации до снятия прошло чуть больше 4 часов; исследователи StepSecurity обнаружили компрометацию в первые 30 минут.
Как именно работала уязвимость
Ключевая техническая деталь — репозиторий использовал GitHub Actions триггер pull_request_target в workflow, который при этом чекаутил код из самого пул-реквеста, а не из базовой ветки:
# УЯЗВИМЫЙ паттерн — не повторяйте
on:
pull_request_target: # запускается с правами базового репозитория, включая секреты
jobs:
build:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # но чекаутит код ИЗ PR
pull_request_target в отличие от обычного pull_request выполняется в контексте базового репозитория — то есть с полным доступом к секретам. Если при этом workflow чекаутит код именно из пул-реквеста (а не из main), любой, кто может открыть PR, может подставить в этот workflow произвольный код, который выполнится с секретами репозитория. Это и называется pwn request — известный, задокументированный паттерн, для которого fix у AsyncAPI уже существовал в виде открытого PR, просто не был смёржен вовремя.
Почему это не остановили ни install-script блокировки, ни npm v12
Здесь важно понимать техническое отличие от атак, которые мы разбирали в других гайдах. Вредоносный код был не в install/postinstall-скрипте, который блокирует npm v12 или требует одобрения через allowScripts — он был встроен прямо в исходный код пакета и срабатывал в момент импорта модуля (require() или import), при первом обращении к нему из вашего кода, тестов, CLI или сборочного скрипта. npm v12 и allowScripts защищают конкретно install-time хуки; они физически не имеют отношения к тому, что происходит после того, как пакет уже установлен и импортирован.
Полезная нагрузка при этом была разделена на две стадии: маленький загрузчик прямо в npm-пакете и основной payload (Miasma RAT), который скачивался отдельно с IPFS по хэшу. Это уменьшает подозрительный след внутри самого npm-пакета — сканеры, которые ищут явные признаки малвари в коде пакета, видят только безобидный на вид загрузчик.
Что бы реально помогло
Staged publishing не спас бы напрямую — публикация шла через легитимный, доверенный CI/CD воркфлоу самого проекта, а не через украденный токен постороннего скрипта. Но правильно настроенные branch protection rules на ветках next и alpha (а не только на main) — спасли бы, потому что атака началась именно с push-доступа к незащищённой pre-production ветке.
Аудит workflow-файлов на использование pull_request_target с чекаутом кода PR — это конкретная, проверяемая вещь. Если в репозитории используется этот паттерн, его нужно закрывать явно: либо не чекаутить PR-код в pull_request_target-воркфлоу вообще, либо использовать pull_request (без доступа к секретам) для шагов, которые должны видеть код пул-реквеста.
GitHub Actions Workflow Permissions Batch Auditor (инструмент) проверяет permissions: в ваших workflow-файлах на избыточные права — это не находит конкретно pwn request паттерн, но снижает ущерб, если такая уязвимость всё же будет проэксплуатирована: воркфлоу с минимальными правами не сможет опубликовать пакет или получить доступ к секретам, которые ему не нужны для его прямой задачи.
Итог
Атака на AsyncAPI — редкий и показательный случай, когда все стандартные меры защиты npm-экосистемы (install-script блокировка, allowScripts, staged publishing, dependency cooldown) технически не касаются вектора атаки, потому что уязвимость была не в npm registry, а в конфигурации GitHub Actions конкретного репозитория. Единственная реальная защита здесь — branch protection на всех ветках, из которых возможна публикация (не только main), и регулярный аудит workflow-файлов на паттерн pwn request.