サプライチェーンセキュリティ
サイトの 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)は設計上無効化されています。 ツリー全体でレジストリから常時取得しても、得られるのは定期的な推移的依存の更新にすぎず、それはアラート駆動の修正がすでにカバーしています。
- Renovate のスケジュールされたロック一括再解決(
- クールダウン済みのリリースのみを解決する: 意図的な解決であっても、クールダウン期間より新しいリリースは無視します。
悪意あるリリースのレジストリ側での削除には数日かかります。
- 設計上の例外が一つあります。 Dependabot セキュリティアップデートは、既知の脆弱性修正を即座に配信します。
- クールダウンはレジストリで解決されるパッケージを対象としています。 Node ツールチェーンのピンはかわりにフロアポリシーに従います。 署名済みのプロジェクトビルドはレジストリの削除遅延リスクを共有しないためです。
- ロックファイルからインストールする: インストールはロック完全一致で行われ、コミット済みでレビュー済みの
- パッケージのインストール時スクリプトは、コントリビューターのホストやビルドマシン上で攻撃者のコードを実行する。これがワームのペイロード経路だった。
- レビュー済みのライフサイクルスクリプトのみを実行する: lifecycle scripts はデフォルト拒否です。
- 承認はバージョン完全一致であるため、侵害されたパッチリリースが前のバージョンの承認を引き継ぐことはできません。
- レビューは拒否も記録するため、記録がないことは常に未レビューを意味します。
- 例外は使用箇所でインラインで名前付きで再有効化されます。 デフォルトの態勢を弱めることはありません。
- レビュー済みのライフサイクルスクリプトのみを実行する: lifecycle scripts はデフォルト拒否です。
- 再有効化された唯一のフックはピン留めされた Hugo バイナリを取得する。インストーラーは、そのフェッチをリポイントまたはアンピンできる環境オーバーライドを尊重する。
- Hugo インストーラーのオーバーライドを拒否する: リビルドラッパーは、それらのいずれかが設定されている間は実行を拒否します。
- Netlify の独自の npm install は無人で実行され、このリポジトリが制御するスクリプトの範囲外であり、無効化できない。
- 自動インストールを無効化する: 設定により自動インストールを無効化し、ビルドコマンドが実際のインストールを実行します。
- レジストリで解決可能な名前で呼び出された bin は、ローカルのインストールが古い場合にその名前を主張する者を実行する。6月に未登録の bin 名のスクワットがこれを証明した。
- bin を呼び出し、名前は呼び出さない: リポジトリの配線は素の
npxを使用しない。 bin はインストール済みの依存関係ツリーから取得されるか、明示的に失敗します。
- bin を呼び出し、名前は呼び出さない: リポジトリの配線は素の
- 制御が暗黙のうちに適用されなくなることは、制御がないことよりも悪い。
適用方法の一覧:
| 決定 | 適用方法 |
|---|---|
| Minimize dependencies | 依存関係レビュー時のメンテナーの判断。機械的な制御はなし |
| Install from the lock | すべてのインストール契約での npm ci |
| Resolve deliberately | 慣例と renovate.json5 での無効化された lockFileMaintenance |
| Resolve only cooled-down releases | npm と 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 警告 |
先行事例
- デフォルト拒否のライフサイクルスクリプトはエコシステムの方向性です。
- リリースクールダウンは確立されたプラクティスです。
- pnpm はデフォルトで1日より新しいリリースを遅延させます。
- Renovate の npm
minimumReleaseAgeセキュリティプリセットは3日間を設定しており、npm の72時間アンパブリッシュウィンドウに対応しています。 ここで使用しているより長い値は、エコシステムの他の箇所で採用されているクールダウンに沿ったものです。
- この制御セットは確立されたフレームワークのガイダンスに対応しています。
- TUF の攻撃分類: 任意のソフトウェアインストール、ミックスアンドマッチ、および余分な依存関係攻撃。
- OpenSSF npm ガイド: ロック完全一致の CI インストール。