技術 約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 viewnpm stage download で確認し、少なくとも次の差分を見る運用になる。

見るもの理由
package.jsonscriptspreinstallinstallpostinstallprepare の追加を拾う
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
.npmrcpackage.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設定は、明示的に見直す。

動き始めた例もある。
postcssnanoid を公開するnpmアカウント ai は、provenance未設定の集中リスクを指摘された後、公開前に承認を挟むstaged releasesへの移行を進めている。

利用側では、11.15.0以上のnpmで非registryソースを閉じる設定を先に試せる。
特にCIで毎回クリーンインストールするプロジェクトは、lockfileに github:git+https://...tgz が混ざっていないかを見てから allow-git=noneallow-remote=none を入れる。

file: やworkspace外ディレクトリを正規に使っているモノレポでは、いきなり全部 none にすると開発フローが壊れる。
その場合でも、CIだけ先に閉じる、またはリリースビルドだけ閉じる、という分け方ができる。

参考