npmに2FA承認つきstaged publishingとinstall元制限が入った
目次
TL;DR
変更 npm CLI 11.15.0以降で staged publishing が一般提供。npm stage publish でtarballをいったんstage queueへ置き、メンテナがCLIまたはnpmjs.comで2FA承認してからregistryへ出す流れ
条件 既存パッケージ、publish権限、npmアカウントの2FA、npm CLI 11.15.0以上、Node 22.14.0以上。新規パッケージの初回公開はstaged publishing対象外
install側 --allow-file、--allow-remote、--allow-directory が追加され、既存の --allow-git と合わせて非registryソースを明示的に閉じられる。現時点の既定は all
npmの防御が、公開前の人間承認とinstall時の取得元制限に分かれてきた。
GitHub Changelogの2026年5月22日告知では、npm CLI 11.15.0以降で staged publishing がGAになり、同時に --allow-file、--allow-remote、--allow-directory が追加された。
前に書いたpnpm 11、Yarn 4.10、npm v11.10のrelease-age gateは、利用側が公開直後の悪意あるバージョンをすぐ取らないための話だった。
今回のstaged publishingは反対側で、パッケージ作者が公開直前に止めるための仕組みになる。
CIはstage queueまで、人間が2FAでregistryへ出す
従来の npm publish は、認証が通ればその場で公開される。
staged publishingでは、CIやローカルから npm stage publish を実行すると、ビルド済みtarballがstage queueに入る。
その時点ではまだ利用者の npm install から見つからない。
公開するには、メンテナが staged package を確認し、npm stage approve <stage-id> かnpmjs.com上のApprove操作で承認する。
この承認時に2FAが要求される。
docsでは npm stage publish 自体は2FA不要、approve側で2FA確認、と分かれている。
流れはこうなる。
flowchart TD
A["CI / local<br/>npm stage publish"] --> B["stage queue<br/>まだinstall不可"]
B --> C["npm stage view / download<br/>tarball確認"]
C --> D["maintainer approve<br/>2FA challenge"]
D --> E["npm registryへ公開<br/>install可能"]
条件もある。
staged publishingを使えるのは、既にnpm registry上に存在するパッケージだ。
新規パッケージの初回公開はstageできない。
ほかに、publish権限、npmアカウントの2FA、npm CLI 11.15.0以上、Node 22.14.0以上が要る。
この制約は小さくない。
初回公開時のタイポスクワッティング(名前を似せた偽パッケージの手口)や、新しいscopeを作って投げるタイプの攻撃はstaged publishingだけでは止まらない。
既存パッケージの更新フローに、公開前の承認点を足す機能だ。
trusted publishingはstage-onlyに絞れる
GitHubは、staged publishingをtrusted publishingと組み合わせる構成を推奨している。
trusted publishingは、GitHub ActionsなどのCIからOIDCでnpmへ公開する仕組みだ。
長期npmトークンをCI secretに置かずに済む。
今回の差分は、trusted publisherの許可アクションを npm stage publish のみにできる点だ。
この設定では、該当CI workflowから直接 npm publish しようとしても拒否され、stage queueへの投入だけが通る。
CIは非対話のままビルドとstage投入まで進み、最後のregistry公開だけを2FA付きの人間承認に戻せる。
ここはMini Shai-HuludのTanStack/Mistral波で見たOIDC悪用とつながる。
TanStack波では、正規GitHub Actions経路からSLSA provenance付きの悪意あるパッケージが出た。
provenanceは「どのCIが作ったか」を示すが、runnerやcacheが汚染されていないことまでは示さない。
stage-onlyのtrusted publisherなら、CI上で短命publish tokenを奪われても、最終公開前にstage queueで止まる。
もちろん、承認者がtarballを確認せずにApproveすれば通る。
それでも「CIが成功したら即registryへ公開」という経路より、見る場所が1つ増える。
stage queueで見るのは差分と出所
staged publishingは、承認画面を足すだけでは弱い。
stage queueに入ったtarballを npm stage view や npm stage download で確認し、少なくとも次の差分を見る運用になる。
| 見るもの | 理由 |
|---|---|
package.json の scripts | preinstall、install、postinstall、prepare の追加を拾う |
| dependency差分 | Mastraの easy-day-js やaxiosの plain-crypto-js のような偽依存を拾う |
| provenance | 正規CI由来か、ローカルや長期トークンpublishかを分ける |
| tarball内の新規ファイル | setup.cjs、ローダー、難読化済みJavaScriptを拾う |
Mastraのnpm侵害では、正規パッケージ本体ではなく、追加された easy-day-js 依存の postinstall が入口だった。
Mastra本体のコード差分だけを見ても、悪意あるコード本体までは出てこない。
stage queueでdependencyとtarballを確認する意味はそこにある。
一方で、staged publishingはメンテナアカウントの2FA承認に依存する仕組みなので、承認者端末が侵害されている場合は防げない。
また、長期間気づかれない悪意ある変更や、レビューをすり抜ける小さな依存追加までは自動で止めない。
「人間が承認する」だけで済ませず、差分を見る対象を先に決めておく。
非registryソースをallow flagsで閉じる
同じ npm CLI 11.15.0 で、install側には3つのフラグが足された。
| フラグ | 閉じる対象 |
|---|---|
--allow-file | ローカルファイルパス、ローカルtarball |
--allow-remote | https://.../package.tgz のようなリモートtarball |
--allow-directory | ローカルディレクトリ |
--allow-git | github:、gitlab:、git+、owner/repo 形式などのGitソース |
値は all または none。
.npmrc や package.json configにも置ける。
現時点では新しい3つも既存の --allow-git も既定値は all なので、閉じたいプロジェクトでは明示する。
# .npmrc
allow-git=none
allow-remote=none
allow-file=none
allow-directory=none
GitHub Changelogは、--allow-git について npm v12 で既定値が none に変わる予定だと書いている。
新しい --allow-file、--allow-remote、--allow-directory は、11.15.0時点では先に厳しくしたい人向けの追加だ。
Git依存は、Mini Shai-Huludの@antv波でそのまま攻撃経路になった。
github:antvis/G2#... のようなoptional dependencyがnpm tarballとは別に落ち、Git依存側のライフサイクルスクリプトが走る。
release-age gateはnpm registry上の公開時刻を見るが、Git commitにはnpmの公開時刻がない。
allow-git=none は、その別経路を依存解決前に止める。
リモートtarballも似ている。
registry上のバージョン履歴、unpublish、deprecate、provenance表示から外れやすい。
https://example.com/pkg.tgz のような依存がlockfileに入るプロジェクトでは、レビューなしで許可しないほうが無難だ。
age gateとallowScriptsとは別の層
5月から6月にかけてnpmまわりの防御が増えたので、どこに当たるかを混ぜないほうがいい。
| 仕組み | 止める場所 | 代表例 |
|---|---|---|
| staged publishing | registry公開前 | CIがstage queueへ入れ、人間が2FA承認 |
| trusted publishing | 公開認証 | 長期npm tokenをCIから消す |
| provenance | 公開経路の証拠 | どのworkflowが作ったかを見る |
| release-age gate | 依存解決 | 公開直後の汚染版を取らない |
| allow source flags | 依存取得元 | Git、remote tarball、file、directoryを閉じる |
| allowScripts | install時実行 | postinstall や暗黙node-gypを許可制にする |
npm v12のinstall scripts既定停止は、取得済みパッケージの自動実行を止める話だった。
今回の --allow-* は、取得元そのものを絞る。
staged publishingは、さらに手前の公開フローに入る。
たとえばMastra波なら、easy-day-js@1.11.22 を公開直後に取らないのがrelease-age gate、取っても postinstall を走らせないのがallowScripts、Mastra側の正規更新を公開前に止める余地があるのがstaged publishingだ。
同じインシデントでも、当たる場所が違う。
既存パッケージのメンテナが変えるところ
自分がnpmパッケージを公開しているなら、まずCIの公開ステップを npm publish から npm stage publish に変える。
trusted publisherを使っている場合は、許可アクションを npm stage publish のみにする。
既存設定が2026年5月20日以前に作られている場合、npm docsでは既存workflowの挙動を変えないため npm publish のみ許可として扱われると説明している。
古いtrusted publisher設定は、明示的に見直す。
動き始めた例もある。
postcss や nanoid を公開するnpmアカウント ai は、provenance未設定の集中リスクを指摘された後、公開前に承認を挟むstaged releasesへの移行を進めている。
利用側では、11.15.0以上のnpmで非registryソースを閉じる設定を先に試せる。
特にCIで毎回クリーンインストールするプロジェクトは、lockfileに github:、git+、https://...tgz が混ざっていないかを見てから allow-git=none と allow-remote=none を入れる。
file: やworkspace外ディレクトリを正規に使っているモノレポでは、いきなり全部 none にすると開発フローが壊れる。
その場合でも、CIだけ先に閉じる、またはリリースビルドだけ閉じる、という分け方ができる。