22 мая 2026 npm выкатил staged publishing — механизм, который разрывает самую опасную часть цепочки атак на реестр: украденный CI-токен больше не может опубликовать пакет напрямую. Токен может только поместить его в очередь на подтверждение, а финальное решение остаётся за живым человеком с 2FA.
Какую именно проблему это решает
Все громкие атаки 2026 года (Shai-Hulud, TeamPCP, компрометация Mastra AI) объединяет одно: злоумышленник получал не пароль мейнтейнера, а токен из CI/CD-пайплайна — долгоживущий NPM_TOKEN, который лежит в секретах репозитория и которым можно опубликовать пакет без каких-либо дополнительных проверок. Кампания TrapDoor (те же дни, что и релиз staged publishing) как раз использовала украденные CI-токены для публикации 34 вредоносных пакетов в 384 версиях сразу на npm, PyPI и Crates.io.
Staged publishing убирает саму возможность: npm publish из CI больше не публикует пакет мгновенно — он попадает в очередь ожидания, и только человек, прошедший 2FA-проверку, может подтвердить релиз.
Как это выглядит в CLI
Требования: npm CLI 11.15.0+ и Node.js 22.14.0+.
# CI (или вручную) — кладёт пакет в очередь, БЕЗ запроса 2FA
cd /path/to/package
npm stage publish
# Посмотреть, что стоит в очереди
npm stage list
# Изучить конкретный staged-пакет перед подтверждением
npm stage view <stage-id>
npm stage download <stage-id>
# Подтвердить и реально опубликовать — ЗДЕСЬ запросит 2FA
npm stage approve <stage-id>
# Или отклонить, если что-то не так
npm stage reject <stage-id>
Ключевая деталь: npm stage publish можно вызвать по любому типу токена, включая OIDC — 2FA не требуется на этом шаге специально, чтобы CI мог оставаться полностью неинтерактивным. Проверка присутствия человека переносится на момент npm stage approve, где 2FA обязателен что через CLI, что через веб-интерфейс npmjs.com.
Пример конфигурации CI: stage-only через trusted publishing
Правильная связка — совместить staged publishing с trusted publishing (OIDC), настроенным в режиме stage-only: тогда прямой npm publish из этого workflow будет отклонён реестром вообще, а пройдёт только npm stage publish.
# .github/workflows/publish.yml
name: Publish package
on:
push:
tags: ["v*"]
permissions:
id-token: write # обязательно для OIDC / trusted publishing
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: npm ci
- run: npm stage publish
В настройках trusted publisher на npmjs.com для этого пакета выставляется разрешение “stage-only” — тогда даже если кто-то вручную поменяет последнюю строку на npm publish, реестр всё равно откажет, потому что этому OIDC-провайдеру разрешено только стейджить, не публиковать напрямую.
Слабое место: монорепозитории
Готового bulk-approve пока нет. Если релиз монорепо публикует 12 пакетов разом, это 12 отдельных npm stage approve с 2FA-подтверждением на каждый — либо вручную по одному в веб-интерфейсе npmjs.com (терпимо для мелких релизов, утомительно для крупных), либо локальным скриптом, который вызывает npm stage approve в цикле — 2FA-промпт всё равно всплывёт на каждый вызов отдельно, просто не нужно каждый раз переоткрывать npmjs.com руками.
Отличие от dependency cooldown и allowScripts
Три меры закрывают разные звенья цепочки, и они не заменяют друг друга:
| Механизм | От чего защищает | На чьей стороне работает |
|---|---|---|
| Staged publishing | Публикация вредоносной версии украденным CI-токеном | Сторона мейнтейнера пакета |
| Dependency cooldown | Автообновление на свежевышедшую вредоносную версию | Сторона потребителя зависимости |
| allowScripts | Выполнение install-скрипта уже установленного пакета | Сторона потребителя зависимости |
Если вы публикуете свои пакеты в npm — staged publishing касается лично вас. Если вы только потребляете чужие зависимости — это работает на вас незаметно, при условии, что мейнтейнеры используемых вами пакетов его включили; повлиять на это напрямую вы не можете, только выбором более защищённых зависимостей при прочих равных.
Итог
Staged publishing опционален — пока вы не поменяли CI на npm stage publish, пакеты публикуются как раньше, никакого автоматического включения. Минимальный набор изменений: две строки в CI-workflow, одна настройка OIDC-провайдера в режиме stage-only, и договорённость внутри команды, кто отвечает за подтверждение. Для монореп стоит сразу закладывать время на серию 2FA-подтверждений при каждом релизе — это единственное практическое неудобство метода.