昨年11月にShai-Huludがnpmを席巻した際(700以上のパッケージが侵害、25,000のリポジトリが露出)、エコシステムは防御策として「ライフサイクルスクリプトを無効化し、ロックファイルをコミットせよ」という定石を採用した。それはGitHubのセキュリティガイドから企業のポリシー文書に至るまで、あらゆる場所で標準的なアドバイスとなった。理にかなっている。インストール時に悪意あるコードが実行できず、依存関係ツリーが固定されていれば、安全なはずだ。そうだろう?

これを考えていて、ふと思った:もし攻撃者がその防御策を飛び越えられる脆弱性があったとしたら?それは大変なことだ。そのような脆弱性が存在するなら、悪意ある者たちより先に自分が見つけるべきだと判断した。そこで、防御策の穴を探し始めた。
その結果、npm、pnpm、vlt、Bunにまたがる6つのゼロデイ脆弱性を発見した。スクリプト実行とロックファイルの整合性、両方に対するバイパスが含まれる。pnpmはパッチを適用済み(CVE-2025-69263、CVE-2025-69264)。vltもパッチ適用済み。Bunも対応済み。一方、現在Microsoftの傘下にあるnpmは私の報告を却下した。「仕様どおりの動作」だという。
現状はこうだ:史上最悪のnpmサプライチェーン攻撃を受けて皆が採用した防御策に大きな欠陥があり、エコシステム最大のパッケージマネージャーはその欠陥を修正する価値がないと判断した。組織が--ignore-scriptsを安全網としてnpmに依存しているなら、その網には今この瞬間も穴が開いている。私たちはこれをPackageGateと呼ぶ。
Shai-Hulud後の定石
Shai-Huludが発生したとき、事後分析は自ずと書かれた。このワームはpostinstallスクリプトを通じて拡散し、npmトークンを奪取して、アクセスできるパッケージの悪意あるバージョンを公開した。サプライチェーン悪用の教科書のような手口であり、他の攻撃者たちも注目していた。Shai-Hulud以降、この手法を利用するマルウェアは急増している。その後の数ヶ月間で、私たちのチームはpostinstallスクリプトを悪用したパッケージを数百件検知している。
数週間のうちに、--ignore-scriptsはパワーユーザー向けのフラグから標準的な推奨事項へと昇格した。セキュリティチームはCIの設定に加え、ブログ記事はそれを必須と呼んだ。ロックファイルについても同様だ:package-lock.jsonをコミットすれば、依存関係ツリーを既知の安全なバージョンに固定できる。予期せぬ事態はない。
この2つのメカニズムが、JavaScriptエコシステムにおけるサプライチェーンリスクへの標準的な答えとなった。スクリプトを無効化し、依存関係を固定すれば、安心して眠れる。
これらの防御策が実際に行うこと
初心者のための簡単な解説。
ライフサイクルスクリプトとは、パッケージのインストール中に自動的に実行されるコマンドだ。npm installを実行すると、パッケージは特定のタイミングで実行されるスクリプトを定義できる:preinstall(インストール前)、install(インストール中)、postinstall(インストール後)。これがShai-Huludの拡散経路だった。侵害されたパッケージがpostinstallスクリプトを実行し、トークンを窃取して他のパッケージに感染していた。
--ignore-scriptsフラグ(または.npmrcのignore-scripts=true)は、これらのスクリプトを完全にスキップするようnpmに指示する。preinstallもpostinstallも、何も実行されない。パッケージはインストールされるが、埋め込まれたスクリプトは休眠状態のままだ。
ロックファイルの仕組みは異なる。最初に依存関係をインストールすると、パッケージマネージャーはロックファイル(npmのpackage-lock.json、pnpmのpnpm-lock.yamlなど)を生成する。これにはツリー内のすべてのパッケージの正確なバージョンと整合性ハッシュが記録される。以降のインストール時、パッケージマネージャーは受信したパッケージをこれらのハッシュと照合する。一致しない場合、インストールは失敗する。
これらのメカニズムを組み合わせることで、「予期しないものは何も実行されず、審査していないものは何もインストールされない」ことが保証されるはずだ。
スクリプト実行防止のバイパス
--ignore-scriptsフラグとその同等機能は、インストール中にコードが実行されるのを防ぐためのものだ。テストした3つのパッケージマネージャーすべてでバイパス方法を発見した。
npmには、使用するgitバイナリを指定するgit設定オプションがある。git依存関係をインストールすると、npmはリポジトリをクローンし、その中でnpm installを実行して、存在する.npmrcファイルを読み込む。攻撃者はgit=./malicious-script.shを指定する悪意ある.npmrcと、ネストされたgit依存関係を含むgit依存関係を作成する。npmがそのネストされた依存関係を処理する際、gitの代わりに攻撃者のスクリプトが実行される。--ignore-scriptsが有効であっても完全なRCEが成立する。過去にこの手法を悪用してリバースシェルを作成する概念実証を公開したアクターの存在についても証拠がある。

pnpm v10はセキュリティ機能として「デフォルトでスクリプト無効」を導入し、onlyBuiltDependenciesと呼ばれる許可リストで実装している。しかしこれはビルドフェーズにのみ適用される。git依存関係は別のフェッチフェーズを経由し、そこではprepare、prepublish、prepackスクリプトが無条件に実行される。セキュリティ設定は一切チェックされない。攻撃者がprepareスクリプトを持つgit依存関係を公開すれば、プロンプトや警告なしに実行される。
vltには、tarball展開時にパストラバーサルのバグがあった。../シーケンスをブロックするための正規表現が、^を文字列の先頭アンカーではなくリテラル文字として扱っていた。package/../../../.bashrcのようなパスは素通りしてしまう。攻撃者はファイルシステム上の任意の場所、たとえばユーザーのgitバイナリを上書きするファイルを書き込むtarballを作成できる。vltがその後内部のgit依存関係をクローンすると、攻撃者のコードが実行される。
BunにはtrustedDependenciesメカニズムがあり、どのパッケージがライフサイクルスクリプトを実行できるかをホワイトリストで管理するはずだった。しかし信頼チェックはパッケージの名前のみを検証し、そのソースは検証しない。Bunにはesbuild、sharp、playwrightなど366の信頼済みパッケージ名のデフォルトリストも同梱されている。攻撃者はこれらの信頼済み名称のいずれかを使って悪意あるtarballまたはgitサブ依存関係を作成でき、Bunはそのスクリプトを喜んで実行する。サブ依存関係にesbuildという名前をつけ、HTTPのURLまたはgitリポジトリを指定するだけで、被害者側に明示的な信頼設定がなくてもpostinstallスクリプトが実行される。

ロックファイル整合性チェックのバイパス
ロックファイルは、昨日インストールしたものと今日インストールするものが同じであることを保証するはずだ。整合性ハッシュがそのメカニズムだ:コンテンツがハッシュと一致しなければ、インストールは失敗する。
pnpmとvltはどちらも、HTTPのtarball依存関係を整合性ハッシュなしで保存していた。これらの「リモートダイナミック依存関係」(任意の場所にホストされたtarballを指すURL)は、コンテンツを検証する手段なしに、URLのみでロックファイルに記録される。
つまり、悪意ある依存関係がHTTPのtarballを指している場合、サーバーはリクエストのたびに異なるコードを返せる。最初のインストール時:セキュリティレビューを通過した無害なコード。CIでの2回目のインストール時:悪意あるペイロード。ロックファイルはコミットされており、URLも変わっていないが、コードは完全に異なる。
依存関係ツリーにパッケージを潜り込ませた攻撃者は(たとえ数層深くても)、タイミング、IPアドレス、その他任意のシグナルに基づいて標的を絞ったペイロードを配信できる。ロックファイルはまったく保護にならない。すでに実際に悪用された事例がある。私たちが10月に検知したキャンペーンPhantomRavenはRDDを使用してnpm上のすべてのセキュリティスキャナーから悪意あるコードを隠蔽し、86,000件以上のダウンロードを獲得した。
開示プロセス
私たちはPackageGateの脆弱性を4つすべてのパッケージマネージャーに報告した。
npmへはHackerOneを通じて脆弱性を申告した。彼らはチケットをクローズした。その説明:「npmユーザーは自身がインストールを選択したパッケージのコンテンツを審査する責任を負う」。また、脆弱性が実際に存在するnpmクライアントではなく、npmレジストリのドキュメントを引用した。報告が十分に精査されたのか疑問が残った。
この対応が特に衝撃的なのは、npmのバグバウンティプログラム自体が明示的に「--ignore-scriptsフラグを用いたパッケージインストール時の任意スクリプト実行」を対象領域として列挙しているからだ。
私たちはドキュメントの誤りを指摘しながら、再考を求めて何度か働きかけた。返答はなかった。最後の手段として、個人的なつながりを使ってnpmチームの誰かに再検討を依頼した。残念ながら、この試みも実を結ばなかった。
他のパッケージマネージャーの対応は異なった。pnpmは小規模なチームで運営されているにもかかわらず、即座に対応し2週間以内に問題を修正した。vltはMicrosoftを去った元npmエンジニアたちが構築した1.0未満のプロジェクトだが、8日間で修正した。Bunは報告を認め、3週間以内にバージョン1.3.5で修正をリリースした。3チームとも専門的で、報告を真剣に受け止め、ユーザーを守るために迅速に行動した。彼らのユーザーセキュリティへのコミットメントを目の当たりにして、本当に心強かった。

しかし、npmが対応を拒否した以上、私たちは困難な決断を迫られた。これほど重要なインフラに存在する未修正の脆弱性を公開することは、軽々しく行うものではない。望んでいたことでもない。しかし現実を無視することもできなかった:私たちが1週間足らずで発見できたなら、国家支援を受けた攻撃者たちはほぼ確実にすでに発見しているだろう。沈黙し続けることの方がリスクが高いと感じ始めた。
そこで、不本意ながらも公開することを決めた。npmを批判するためではなく、人々や組織が何に依存しているかを把握し、自ら情報に基づいた選択を行えるようにするためだ。セキュリティを真剣に考える代替パッケージマネージャーも存在する。このポストが不要になる状況を望んでいた。
実際に取るべき対策
標準的なアドバイスが間違っているわけではない。ただ不完全なだけだ。引き続き以下を実施すべきだ:
- ロックファイルをソース管理にコミットする。これはリモートダイナミック依存関係や予期せぬバージョン変更に対する最善の防御策だ。まだ実施していないなら、今日から始めよう。
--ignore-scriptsまたはその同等機能を使用する。npmには--ignore-scripts、pnpmにはenable-scripts=false、BunにはtrustedDependenciesがある。有効にしよう。- パッケージマネージャーを最新バージョンに更新する。このような脆弱性は定期的に発見され、新しいリリースでパッチされる。使用しているパッケージマネージャーを常に最新の状態に保とう。
- パッケージマネージャーの選択を検討する。pnpmはnpmよりも厳格なデフォルト設定とより細かなセキュリティコントロールを提供している。vltはまだ1.0未満だが、npmの弱点を熟知した人々が設計しており、それらを克服すべく構築されている。この分野での競争は健全だ。
最後に
このポストは誰かを批判するために書いたのではない。JavaScriptエコシステムはもっと良くなるべきだと思うから、そして機能しない防御策への思い込みではなく、正確な情報に基づいてセキュリティの意思決定を行うべきだから書いた。
スクリプトを無効化してロックファイルをコミットするという標準的なアドバイスは、引き続き実践する価値がある。しかしそれが完全な絵ではない。PackageGateが完全に対処されるまで、組織はリスクについて自ら情報に基づいた選択をする必要がある。
この調査はKoiのチームが実施した。私たちがKoiを構築したのは、まさにこのような脅威を捉えるためだ:このポストが明らかにするパッケージマネージャーの防御の隙間を突く攻撃者たちを。npmのセキュリティメカニズムはバイパスされ得るが、私たちのリスクエンジンWingsはインストール中にパッケージが実際に何をするかを監視している。ネットワークリクエスト、ファイルシステムへのアクセス、スクリプト実行、そしてコードがどのように到達したかに関わらず悪意ある意図を明らかにする行動パターンを捉える。
Fortune 50企業や世界最大規模のテック企業から信頼されているKoiは、セキュリティチームが自社環境に流入するサードパーティコードの可視性とガバナンスを確保する支援をしている。
デモを予約して、従来の防御をすり抜ける脅威を私たちのエージェンティックAIリスクエンジンがどのように捉えるかをご覧ください。
お気をつけて。
開示タイムライン
npm --ignore-scripts バイパス
- 2025-11-26: HackerOneへ最初の報告を提出(#3442684)
- 2025-11-26: ベンダーが受領を確認
- 2025-12-05: 「情報提供」としてクローズ — ベンダーは意図的な設計と主張
- 2025-12-06: クローズに異議;–ignore-scriptsはRCEを防ぐべきと指摘
- 2025-12-07: 他のJSパッケージマネージャーが同様の報告を受理したことを指摘
- 2025-12-08: ベンダーが誤ったドキュメントを引用したことを指摘(レジストリ対CLI);調停を要請
- 2025-12-11: 再検討を求めてnpmエンジニアに直接メール(紹介経由)(返信なし)
- 2025-12-18: 公開開示の予定をベンダーに通知
ステータス:「情報提供」としてクローズ
翻訳元: https://www.koi.ai/blog/packagegate-6-zero-days-in-js-package-managers-but-npm-wont-act