技術約8分で読めます

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 の scriptspreinstall、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-remotehttps://.../package.tgz のようなリモートtarball
--allow-directoryローカルディレクトリ
--allow-gitgithub:、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 publishingregistry公開前CIがstage queueへ入れ、人間が2FA承認
trusted publishing公開認証長期npm tokenをCIから消す
provenance公開経路の証拠どのworkflowが作ったかを見る
release-age gate依存解決公開直後の汚染版を取らない
allow source flags依存取得元Git、remote tarball、file、directoryを閉じる
allowScriptsinstall時実行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だけ先に閉じる、またはリリースビルドだけ閉じる、という分け方ができる。

参考