PackageGate: JSパッケージマネージャーに存在する6件のゼロデイ脆弱性、npmは対応を拒否

昨年11月にShai-Huludがnpmを席巻し(700以上のパッケージが侵害され、25,000のリポジトリが露出)、エコシステムはある防御策に落ち着きました。ライフサイクルスクリプトを無効化し、ロックファイルをコミットするというものです。この対策は、GitHubのセキュリティガイドから企業のポリシー文書に至るまで、あらゆる場所で標準的な推奨事項となりました。理屈は通っています。インストール時に悪意あるコードが実行できず、依存関係ツリーが固定されていれば、安全だというわけです。本当にそうでしょうか。

Image

ここで一つの疑問が浮かびました。もしこうした防御策を丸ごと迂回できる脆弱性が存在したらどうなるのか、というものです。それは大変な事態です。そこで、もしそのような脆弱性が実在するなら、悪意ある攻撃者に先んじて発見しておくべきだと考えました。そうして、この防御策の抜け穴を探し始めました。

その結果、npm、pnpm、vlt、Bunにまたがる6件のゼロデイ脆弱性を発見しました。スクリプト実行の防止とロックファイルの整合性検証の両方に対するバイパス手法です。pnpmは修正済みです(CVE-2025-69263CVE-2025-69264)。vltも修正済み、Bunも修正済みです。一方、現在Microsoft傘下にあるnpmは、報告されたチケットを「想定通りの挙動」として close しました。

つまり現状はこうです。史上最悪の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など)を生成し、ツリー内のすべてのパッケージの正確なバージョンと整合性ハッシュを記録します。以降のインストールでは、パッケージマネージャーは受け取ったパッケージをこれらのハッシュと照合します。一致しない場合、インストールは失敗します。

この2つの仕組みが組み合わさることで、「意図しないものは何も実行されない」「検証していないものは何もインストールされない」ことが保証されるはずでした。

スクリプト実行防止のバイパス

--ignore-scriptsフラグおよびそれに相当する各パッケージマネージャーの機能は、インストール中のコード実行を防ぐためのものです。しかし私たちは、検証した3つのパッケージマネージャーすべてでバイパス手法を発見しました。

npmには、使用するgitバイナリを指定するgit設定オプションがあります。gitの依存関係をインストールする際、npmはリポジトリをクローンし、その中でnpm installを実行します。その際、存在する.npmrcファイルが読み込まれます。攻撃者は、git=./malicious-script.shを指すよう仕込んだ悪意ある.npmrcを含むgit依存関係と、さらにネストしたgit依存関係を作成します。npmがこのネストした依存関係を処理する際、gitの代わりに攻撃者のスクリプトを実行してしまいます。--ignore-scriptsを有効にしていても、完全なリモートコード実行(RCE)が成立します。実際、この手法を悪用してリバースシェルを作成する概念実証(PoC)が過去に攻撃者によって公開された証拠も確認しています。

Image

pnpm v10は、セキュリティ機能として「デフォルトでスクリプトを無効化」する仕組みを導入し、onlyBuiltDependenciesと呼ばれる許可リストによって実装しています。しかしこれはビルドフェーズにのみ適用されるものです。git依存関係は別のフェッチフェーズを経由し、そこではprepare、prepublish、prepackの各スクリプトが無条件に実行されます。このセキュリティ設定は一切チェックされません。攻撃者がprepareスクリプトを含むgit依存関係を公開すれば、プロンプトも警告もなくそのまま実行されてしまいます。

vltには、tarball展開処理にパストラバーサルのバグがありました。../というシーケンスをブロックするための正規表現が、^を文字列先頭のアンカーではなく、リテラル文字として扱っていたのです。そのためpackage/../../../.bashrcのようなパスがそのまま通過してしまいました。攻撃者は、ファイルシステム上の任意の場所(ユーザーのgitバイナリの上書きを含む)にファイルを書き込むtarballを細工できます。その後vltが内部のgit依存関係をクローンする際、攻撃者のコードが実行されることになります。

Bunには、どのパッケージがライフサイクルスクリプトを実行できるかをホワイトリスト化するtrustedDependenciesという仕組みがあります。しかしこの信頼チェックは、パッケージの名前のみを検証し、その出所は検証しません。さらにBunは、デフォルトで366個の信頼済みパッケージ名のリスト(esbuild、sharp、playwrightなど)を同梱しています。攻撃者は、これらの信頼済み名のいずれかを使った悪意あるtarballやgitサブ依存関係を作成できます。すると Bunはそのスクリプトを喜んで実行してしまいます。サブ依存関係にesbuildという名前を付け、http URLやgitリポジトリを指定するだけで、被害者側の明示的な信頼設定という要件を回避してpostinstallスクリプトが実行されるのです。

Image

ロックファイル整合性チェックのバイパス

ロックファイルは本来、「昨日インストールしたものと今日インストールするものが同一である」ことを保証するはずのものです。その仕組みが整合性ハッシュです。内容がハッシュと一致しなければ、インストールは失敗します。

pnpmvltはいずれも、HTTP経由のtarball依存関係を整合性ハッシュなしで保存していました。こうした「リモート動的依存関係(remote dynamic dependencies)」(どこにホストされたtarballでも指せるURL)は、URLのみでロックファイルに記録され、内容を検証する手段が一切ありません。

つまり、悪意ある依存関係はHTTP経由のtarballを指定でき、そのサーバーはリクエストされるたびに異なるコードを返すことができるのです。1回目のインストールではセキュリティレビューを通過する無害なコードを、2回目のCI上でのインストールでは悪意あるペイロードを返す、といった具合です。ロックファイルはコミット済みで、URLも変わっていないにもかかわらず、実際に実行されるコードはまったく別物になり得ます。

依存関係ツリーに(たとえ何層も深いところであっても)パッケージを紛れ込ませることに成功した攻撃者は、タイミングやIPアドレス、その他任意のシグナルに基づいて標的を絞ったペイロードを配信できます。ロックファイルは何の保護にもなりません。実際、こうした手口はすでに現実で確認されています。私たちが10月に検出したPhantomRavenというキャンペーンは、このRDD(リモート動的依存関係)を使ってnpm上のあらゆるセキュリティスキャナーから悪意あるコードを隠し、86,000件以上のダウンロードを獲得していました。

開示プロセス

私たちはPackageGateの脆弱性を4つのパッケージマネージャーすべてに報告しました。

npmにはHackerOne経由で脆弱性を届け出ましたが、彼らはこのチケットを close しました。その説明はこうでした。「npmのユーザーは、インストールするパッケージの内容を検証する責任を負っている」。さらに彼らは、脆弱性が実際に存在するnpmクライアントではなく、npmレジストリのドキュメントを引用していました。この報告が果たして十分にレビューされたのか、疑問が残ります。

特に印象的だったのは、npm自身のバグバウンティプログラムが、「--ignore-scriptsフラグを設定していてもパッケージインストール時に任意のスクリプトが実行される問題」を対象領域として明示的に挙げているという点です。

私たちはドキュメントの誤りを指摘しながら、再検討を何度も依頼しましたが、返信はありませんでした。最後の手段として、個人的なつながりを通じてnpmチームの誰かに再考を促そうと試みましたが、これも実を結びませんでした。

一方、他のパッケージマネージャーの対応はまったく異なるものでした。pnpmは小規模なチームで運営されているにもかかわらず、即座に応答し、2週間足らずで問題を修正しました。vltはMicrosoftを離れた元npmエンジニアたちによって開発されているバージョン1.0未満のプロジェクトですが、8日間で修正しました。Bunは報告を認め、バージョン1.3.5で3週間以内に修正を出荷しました。この3チームはいずれもプロフェッショナルな対応で報告を真摯に受け止め、迅速にユーザーを守るための行動を取りました。ユーザーのセキュリティに対するこれほどの真摯な姿勢を目にできたことは、率直に励みになりました。

Image

しかし、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月5日:「Informative」として報告が close される — ベンダーは意図した設計であると主張
  • 2025年12月6日:close の判断に異議を申し立て — –ignore-scriptsはRCEを防止すべきものであると指摘
  • 2025年12月7日:他のJSパッケージマネージャーが同様の報告を受理していることを指摘
  • 2025年12月8日:ベンダーが誤ったドキュメント(クライアントではなくレジストリ)を引用していることを指摘し、仲裁を要請
  • 2025年12月11日:(紹介を通じて)npmのエンジニアに直接メールを送り、再確認を依頼(返信なし)
  • 2025年12月18日:ベンダーに一般公開の予定を通知

ステータス:Informativeとして close 済み

翻訳元: https://www.koi.ai/blog/packagegate-6-zero-days-in-js-package-managers-but-npm-wont-act

本記事は koi.ai の記事を翻訳・要約したものです。