3 ポイント 投稿者 GN⁺ 2025-12-19 | 1件のコメント | WhatsAppで共有
  • Seattle Times は Shai-Hulud 2.0 攻撃を偶然回避したが、運にセキュリティ戦略は任せられないという前提でクライアント側防御を導入した
  • npm の Trusted publishing / provenance / granular access tokens のような改善は「公開(publishing)」面を強化する一方、「インストール・更新」時点での悪意あるコード実行は防げないという空白が残っている
  • pnpm は npm レジストリをそのまま使いながらも、消費(install/update)段階で悪意あるパッケージの実行を困難にする制御を追加できる構造になっている
  • パイロットでは pnpm の 3 つの制御を適用し、ライフサイクルスクリプト実行、最新リリースの即時インストール、信頼性のダウングレード といったベクトルをそれぞれ防ぐよう設計した
  • 例外は失敗ではなく設計の一部と捉え、例外を文書化しつつ他のレイヤーが保護を継続する defense-in-depth 運用を目指している

事件の背景と前提

  • 2025 年 11 月、自己複製型 npm ワームが 796 個のパッケージに感染し、月間 1 億 3,200 万ダウンロード規模で拡散した事例が発生した
  • 攻撃は preinstall スクリプトを利用して認証情報を窃取し、永続的なバックドアを設置し、一部環境では開発環境の削除まで引き起こした
  • 私たちの組織に影響がなかった理由は強固な防御ではなく、攻撃期間中に npm install / npm update を実行しなかったという偶然だった
  • ニュース組織にとって信頼は中核であり、サプライチェーン侵害によって顧客データ・従業員認証情報・本番インフラ・ソースコードが露出し、復旧や通知のコストも大きい

チームと導入の文脈

  • Seattle Times は長年デフォルトのパッケージマネージャーとして npm を使ってきており、Yarn を試したこともあったが定着しなかった
  • pnpm を導入する理由は、レジストリレベルの改善を補完するクライアント側セキュリティ制御にある
  • pnpm は同じレジストリ・同じコマンド・同じワークフローを使う drop-in replacement であり、移行可能性が高いと判断した
  • これは完成済みのケーススタディではなく、実際のチームがサプライチェーンセキュリティに取り組み始める際に直面する問題や思考過程を共有するものだ

クライアント側制御が必要な理由

  • npm のセキュリティ改善により、アカウント乗っ取り後の悪意あるパッケージ公開は以前より難しくなった
  • これらの改善は「公開(publishing)」面を保護するが、「消費(consuming)」段階で悪意あるパッケージのインストール自体を防ぐわけではない
  • npm install / npm update 時の lifecycle scripts(preinstall/postinstall など) は、パッケージの安全性評価より前に、開発者権限で任意のコードを実行できる構造になっている
  • スクリプトは npm / GitHub / AWS / DB の認証情報、ソースコード、クラウドインフラ、ファイルシステム全体にアクセスできる
  • Shai-Hulud のような攻撃はこの構造を悪用する。メンテナーのアカウントが侵害されると、悪意あるスクリプトは インストールの瞬間 に実行され、コミュニティが検知する前に被害が発生しうる
  • npm の公開側改善と pnpm の消費側制御を相互補完的な防御として組み合わせ、defense-in-depth を構成する

適用した 3 つのレイヤー

  • パイロットでは、異なる攻撃ベクトルを狙った 3 つの制御を併用した
  • 各制御には現実的な例外のための逃げ道があり、実運用では例外が必要になることを前提に設計している

Control 1: Lifecycle Script Management

  • pnpm はデフォルトで lifecycle scripts をブロックでき、インストールは警告付きで進行できる
  • 警告は無視されうるという懸念から strictDepBuilds: true を選び、スクリプトがあればインストールを即座に失敗させるよう強制した
  • 設定例は pnpm-workspace.yaml の次のフィールドで構成される
    • strictDepBuilds: true
    • onlyBuiltDependencies: 必要なビルドスクリプトを持つパッケージの許可リスト
    • ignoredBuiltDependencies: 不要なビルドスクリプトを持つパッケージのブロック(または無視)リスト
  • 「必要なスクリプト」は、ネイティブ拡張のコンパイルやプラットフォーム依存ライブラリのリンクのような動作として定義する
  • 「不要なスクリプト」は、最適化や任意設定のように、チームの使い方では機能に影響しないものとして定義する
  • インストール失敗によって次のステップを強制する
    • pnpm がどのパッケージにスクリプトがあるかを明確に示す
    • スクリプトの動作を調査して理解する
    • 許可/ブロックを人の判断で意識的に決定し、文書化する
  • pnpm チームは v11 で strictDepBuilds: true をデフォルトにすることを検討しており、allow/deny 構文の名称改善も検討中だ

Control 2: Release Cooldown

  • 最近公開されたバージョンは、一定のクールダウン期間中インストールを禁止し、コミュニティが悪意あるパッケージを検知・削除する時間を確保する
  • 設定例は pnpm-workspace.yaml の次のフィールドで構成される
    • minimumReleaseAge: <duration-in-minutes>
    • minimumReleaseAgeExclude: 緊急ホットフィックスなどの例外許可リスト
  • 「最新が最善」という習慣を捨て、サプライチェーンの観点では少し古いほうが安全な場合があるという発想への転換が必要だ
  • 2025 年 9 月の攻撃(debug、chalk を含む 16 パッケージ)では約 2.5 時間で削除され、2025 年 11 月の Shai-Hulud 2.0 では約 12 時間かかった
  • 組織のリスク許容度に応じてクールダウンは時間・日・週単位にでき、どの形であってもこの攻撃は防げたはずだ
  • 組織はもともと常に最新を使えるわけではないという現実にも合っており、クールダウンは業務を大きく妨げない
  • セキュリティパッチや重大バグなど本当に必要な場合は、レビュー後に例外として解除できる

Control 3: Trust Policy

  • 以前のバージョンより弱い認証で公開されたバージョンが現れた場合、インストールをブロックする
  • メンテナーアカウントが侵害され、公式 CI/CD ではなく攻撃者のマシンから公開されたケースをシグナルとして見ると説明している
  • 設定例は pnpm-workspace.yaml の次のフィールドで構成される
    • trustPolicy: no-downgrade
    • trustPolicyExclude: CI/CD 移行などの例外許可リスト
  • npm はパッケージ公開について 3 段階の信頼レベルを追跡していると説明する(強→弱)
    • Trusted Publisher: GitHub Actions + OIDC + npm provenance ベース
    • Provenance: CI/CD からの署名付き attestation
    • No Trust Evidence: username/password またはトークンベースの公開
  • 新しいバージョンの信頼レベルが以前より低いと、インストールは失敗する
  • 2025 年 8 月の s1ngularity 攻撃 では、攻撃者が CI/CD へのアクセスなしにローカルから悪意あるバージョンを公開し、provenance がなかった事例があり、この制御があればインストールを防げたはずだ
  • 正当なダウングレードの可能性として、新しいメンテナーの参加、CI/CD 移行、CI/CD 障害時の手動ホットフィックスなどを挙げ、調査後に例外リストへ追加する
  • この機能は 2025 年 11 月に pnpm に追加された新機能であり、実際に正当なダウングレードがどれほど頻繁に起きるかはまだ学習中だ

レイヤー組み合わせの動作例: React 脆弱性パッチ

  • 2025 年 12 月に公開された React Server Components の重大な脆弱性パッチを即時適用しなければならないシナリオでは
  • 通常はクールダウンが「たった今公開されたバージョンのインストール」を止めるが、重大なセキュリティパッチでは待てない
  • この場合、minimumReleaseAgeExclude に特定の React バージョンを追加するが、脆弱性告知とパッチの正当性を確認したうえで例外を適用する
  • 例外を適用しても、他のレイヤーは引き続き保護する
    • React には通常 lifecycle scripts がないため、パッチ版にスクリプトが追加されていれば即座に疑わしいシグナルとなり、ブロックされうる
    • 攻撃者が認証情報を盗んでローカルから「パッチ」を公開した場合、信頼レベルのダウングレードによってブロックされうる
  • 例外は「セキュリティ失敗」ではなく、1 つのレイヤーを迂回しても他のレイヤーが残ることで、単一障害点をなくす設計である

パイロット適用結果

  • あるバックエンドサービス 1 つに 3 つの制御をすべて適用して PoC を行った
  • 調査・理解・アクセス定義までの準備時間は合計で数時間程度だった
  • pnpm は lifecycle scripts を持つパッケージ 3 つを特定した
    • esbuild: CLI 起動をミリ秒単位で最適化するが、チームは JS API しか使わないため不要と判断した
    • @firebase/util: クライアント SDK の自動構成だが、チームはサーバー SDK しか使わないため不要と判断した
    • protobufjs: スキーマ互換性チェックだが、推移的依存関係としてのみ使われており、チームのユースケースでは不要と判断した
  • ドキュメント確認とスクリプト分析(AI によるスクリプト解釈支援を含む)を経て、3 つのスクリプトはいずれもチームのユースケースでは不要と結論づけてブロックした
  • 機能への影響はなかった
  • 摩擦(friction)は意図された機能であり、環境で実行されるコードを暗黙に信頼しないようにする強制装置である
  • 新しい依存関係にスクリプトがある場合、レビューと文書化には約 15 分程度かかると見込んでいる

運用して得た学び

  • client-side レイヤーと npm の publishing-side 改善の組み合わせによって、defense-in-depth が実際に機能すると実感した
  • 例外を適用しても他のレイヤーが残るため、例外に対する不安が減る
  • 利便性優先からセキュリティ優先への精神モデルの転換には時間が必要だが、慣れると自然に感じられる
  • 専任セキュリティチームがない中規模組織でも実用的に適用できる
  • trust policy は導入から数週間しか経っていない機能なので、正当なダウングレードの頻度や運用感覚は今後さらに学ぶ必要がある
  • 近いうちに他のコードベースへ展開する計画であり、依存関係グラフが異なるアプリケーションでさらにデータが蓄積される見込みだ

他チーム向けの適用のヒント

  • まず 1 つのプロジェクトから始めて、ワークフローと摩擦点を学ぶ方法を推奨する
  • lifecycle scripts、release cooldown、trust downgrade のいずれでも例外が必要になりうるため、最初から例外を前提に設計すること
  • 警告ベースより、インストール失敗で強制する strictDepBuilds: true を初日から使う戦略を勧める
  • すべての例外を文書化して監査証跡を残し、将来整理しやすくすること
  • あるレイヤーの例外があっても、他のレイヤーの保護は残ることを忘れないこと

1件のコメント

 
bichi 2025-12-19

pnpm! pnpm! pnpm! やっぱり信頼してるよ