サプライチェーンセキュリティ

サイトの npm 依存関係制御の背景にある脅威モデルと根拠

制御の詳細と日常の手順については、Dependency management を参照してください。 関連するセキュリティトピックにはそれぞれ専用のページがあります。 これらの制御を検証する監査の設計については Supply-chain audit design を、ワークフローのトリガーとトークン権限については CI workflows を、脆弱性の報告については security policy を参照してください。

脅威モデル

2026年8月の npm ワーム(security notice)が現在のセキュリティ態勢を定めました。 侵害されたメンテナーアカウントから、人気のある npm パッケージの悪意あるバージョンが公開されました。 パッケージのインストール時の lifecycle scripts がペイロードを実行し、窃取した認証情報を使って伝搬しました。 このリポジトリのいくつかの PR ブランチが封じ込め前に影響を受けましたが、main や本番環境には到達しませんでした。

このリポジトリに関連する攻撃経路は以下のとおりです。

  • バージョン解決: バージョン範囲を解決するインストールは、新しく公開された悪意あるリリースを取得する可能性があります。
  • ライフサイクルスクリプト: インストール時のスクリプト実行により、悪意あるパッケージがコントリビューターのホスト、CI ランナー、ビルドイメージを侵害できます。
  • 無人インストール: CI ジョブや Netlify のビルドイメージは人間の監視なしにインストールを実行します。 エージェントセッションも同様です。
  • 名前解決: レジストリに名前で到達できるツール呼び出し(npx)は、ローカルのインストールが古いか存在しない場合、その名前を保持しているパッケージを実行します。

設計上の決定

共通するテーマはフェイルクローズです。 制御を強制できない場合、制御なしで続行するのではなく、インストールが失敗します。

各決定は攻撃経路に対応しており、おおむね作用するタイミング順に並べています。 末尾の表で決定とその適用方法を対応付けています。

  • 直接的・推移的を問わず、すべての依存関係は攻撃者が到達できる攻撃対象領域である。
    • 依存関係を最小化する: 未使用の依存関係や利便性のためだけの依存関係は、維持するのではなく削除します。
  • バージョン範囲を解決するインストールは、新しく公開された悪意あるリリースを取得する可能性がある。
    • ロックファイルからインストールする: インストールはロック完全一致で行われ、コミット済みでレビュー済みの package-lock.json を再現します。 唯一の例外として、ローカルの npm install は不一致のロックファイルを書き換えることがあります。 検証がそのような書き換えを検出します。
    • 意図的に解決する: バージョン解決は意図的な依存関係更新でのみ行われ、インストールの副作用としては行われません。
      • Renovate のスケジュールされたロック一括再解決(lockFileMaintenance)は設計上無効化されています。 ツリー全体でレジストリから常時取得しても、得られるのは定期的な推移的依存の更新にすぎず、それはアラート駆動の修正がすでにカバーしています。
    • クールダウン済みのリリースのみを解決する: 意図的な解決であっても、クールダウン期間より新しいリリースは無視します。 悪意あるリリースのレジストリ側での削除には数日かかります。
      • 設計上の例外が一つあります。 Dependabot セキュリティアップデートは、既知の脆弱性修正を即座に配信します。
      • クールダウンはレジストリで解決されるパッケージを対象としています。 Node ツールチェーンのピンはかわりにフロアポリシーに従います。 署名済みのプロジェクトビルドはレジストリの削除遅延リスクを共有しないためです。
  • パッケージのインストール時スクリプトは、コントリビューターのホストやビルドマシン上で攻撃者のコードを実行する。これがワームのペイロード経路だった。
    • レビュー済みのライフサイクルスクリプトのみを実行する: lifecycle scriptsデフォルト拒否です。
      • 承認はバージョン完全一致であるため、侵害されたパッチリリースが前のバージョンの承認を引き継ぐことはできません。
      • レビューは拒否も記録するため、記録がないことは常に未レビューを意味します。
      • 例外は使用箇所でインラインで名前付きで再有効化されます。 デフォルトの態勢を弱めることはありません。
  • 再有効化された唯一のフックはピン留めされた Hugo バイナリを取得する。インストーラーは、そのフェッチをリポイントまたはアンピンできる環境オーバーライドを尊重する。
    • Hugo インストーラーのオーバーライドを拒否する: リビルドラッパーは、それらのいずれかが設定されている間は実行を拒否します。
  • Netlify の独自の npm install は無人で実行され、このリポジトリが制御するスクリプトの範囲外であり、無効化できない。
  • レジストリで解決可能な名前で呼び出された bin は、ローカルのインストールが古い場合にその名前を主張する者を実行する。6月に未登録の bin 名のスクワットがこれを証明した。
    • bin を呼び出し、名前は呼び出さない: リポジトリの配線は素の npx を使用しない。 bin はインストール済みの依存関係ツリーから取得されるか、明示的に失敗します。
  • 制御が暗黙のうちに適用されなくなることは、制御がないことよりも悪い。
    • 古い npm ではフェイルクローズする: アクティブな npm が .npmrc の設定を適用するには古すぎる場合、続行するのではなくインストールが失敗します。
    • 信頼せず検証する: 無効化とロック完全一致は、前提ではなく検証済みの主張です。

適用方法の一覧:

決定適用方法
Minimize dependencies依存関係レビュー時のメンテナーの判断。機械的な制御はなし
Install from the lockすべてのインストール契約での npm ci
Resolve deliberately慣例renovate.json5 での無効化された lockFileMaintenance
Resolve only cooled-down releasesnpm と Renovate でのクールダウン
Run only reviewed lifecycle scripts厳格モードの許可リスト。未レビューの場合はインストールが失敗する
Refuse Hugo installer overridesリビルドラッパーの環境スクリーン。リビルド試行前に実行
Neutralize the auto-install自動インストール無効化制御
Invoke bins, not names素の npx 禁止ルール。レビュー規律による制御であり、機械的な制御はなし
Fail closed on old npm厳格なエンジンチェック付きの npm エンジンフロア
Verify, don’t trustサプライチェーン監査クリーンワーキングツリーチェック、ローカルでのロック書き換え時の postinstall 警告

先行事例

  • デフォルト拒否のライフサイクルスクリプトはエコシステムの方向性です。
    • pnpmYarn はデフォルトで依存関係のスクリプトをブロックします。
    • npm の承認済み RFC #54allowScripts を通じて同じモデルを npm にもたらします。 バージョン完全一致のエントリも含まれています。
  • リリースクールダウンは確立されたプラクティスです。
    • pnpm はデフォルトで1日より新しいリリースを遅延させます。
    • Renovate の npm minimumReleaseAge セキュリティプリセットは3日間を設定しており、npm の72時間アンパブリッシュウィンドウに対応しています。 ここで使用しているより長い値は、エコシステムの他の箇所で採用されているクールダウンに沿ったものです。
  • この制御セットは確立されたフレームワークのガイダンスに対応しています。
    • TUF の攻撃分類: 任意のソフトウェアインストール、ミックスアンドマッチ、および余分な依存関係攻撃。
    • OpenSSF npm ガイド: ロック完全一致の CI インストール。