Shai-Hulud
感染したnpmパッケージがインストールされると実行される自己増殖型のワームで、npm、GitHub、クラウドのトークンを盗み、被害者の他のパッケージの感染バージョンを公開します。感染の大半はCI内で発生しました。
ビルドを確認する方法 →レッドチーム vs ブルーチーム
攻撃者が自社を直接狙うことはまれです。狙うのは、あらゆる企業が依存しているパッケージ、Action、ビルドツールであり、そのうえで全員に一斉にエクスプロイトを試します。攻撃者には自社の依存関係の一覧は必要ありません。必要なのは自社のほうです。
ソフトウェアサプライチェーン攻撃は、製品を直接攻撃するのではなく、オープンソースパッケージ、CI/CDコンポーネント、ビルドシステムなど、製品を構成するものを侵害します。現代のソフトウェアの大部分は他者が書いたコードです。その多くはオープンソースパッケージから来ており、さらにその多くはチームの誰も選んでいない推移的依存関係です。広く使われているコンポーネントを1つ侵害すれば、それを基盤とするすべての組織に到達できます。
これにより、攻撃者の標的の選び方が変わります。1社の依存関係を調べるのではなく、影響範囲の大きいパッケージやツールを選び、あらゆる場所でそれを悪用して、何が応答するかを確認するのです。
人気のオープンソースパッケージの欠陥が公開されると、攻撃者は脆弱なバージョンを出荷しているすべての製品でそれを悪用します。多くの場合、それは誰も直接選んでいない推移的依存関係です。
攻撃者がメンテナーのアカウントを乗っ取るか、ワームが公開用のトークンを盗み、信頼されたパッケージの汚染されたバージョンをリリースします。Shai-Hulud npmワームのように、ビルドがインストールした瞬間に実行されます。
コントリビューターが時間をかけてオープンソースプロジェクトの信頼を得たうえで、下流のディストリビューションや製品が取り込むリリースに悪意あるコードを紛れ込ませます。xz utilsのバックドアがその例です。
人気パッケージに似た名前のパッケージが入力ミスを狙います。Dependency Confusionはさらに踏み込み、企業の内部パッケージと同じ名前の公開パッケージが、内部パッケージの代わりにインストールされます。
GitHub Actionなどのビルドツールが乗っ取られ、それを使うすべてのパイプラインが、シークレットに手が届く状態で攻撃者のコードを実行します。tj-actionsやCodecovがその例です。
ビルドそのものが改変されるか、ビルドとデプロイの間で成果物がすり替えられるため、クリーンなソースツリーからでも侵害されたリリースが生まれます。SolarWindsがその例です。
最もよく知られたサプライチェーン攻撃はSolarWindsです。ビルドで悪意あるコードが挿入され、通常のアップデートとして顧客に出荷されました。最近の攻撃は、パッケージレジストリやCIパイプラインを狙います。そこでは、侵害されたパッケージやActionが1つあれば、それを使うすべてのビルドに到達できます。新しい順に紹介します。
感染したnpmパッケージがインストールされると実行される自己増殖型のワームで、npm、GitHub、クラウドのトークンを盗み、被害者の他のパッケージの感染バージョンを公開します。感染の大半はCI内で発生しました。
ビルドを確認する方法 →CISAの報告によると、この人気のGitHub Actionはすべてのバージョンが3日間侵害され、シークレットがワークフローのログに露出しました。タグで参照していたワークフローは攻撃者のコードを実行しました。
Andres Freundは、SSHログインのCPU使用率が高すぎることに気づき、xzのバージョン5.6.0と5.6.1にバックドアを発見しました。バックドアは上流プロジェクトそのものに仕込まれていました。
Apache Log4j 2の欠陥により、ログに記録されるメッセージを制御できる攻撃者が任意のコードを実行できました。CVSSは10.0です。多くのチームにとって難しかったのは、どのアプリケーションにlog4j-coreが含まれているかを見つけることでした。たいていは依存関係の依存関係として含まれていました。
攻撃者は、チームがCIパイプラインで実行していたBash Uploaderスクリプトを改変し、認証情報、トークン、キーを含むCI環境をCodecovのインフラ外のサーバーに送信させました。1月31日から、顧客が4月1日に報告するまで気づかれずに実行されていました。
攻撃者はSolarWinds Orionのビルドプロセスに長期間ひそかにアクセスし、Orionのリリース2019.4から2020.2.1 HF1に悪意あるコードを挿入しました。顧客はそれを通常のアップデートとしてインストールしました。CISAはこれを受けて緊急指令を発出しました。
レッドチーム vs ブルーチーム
各段階を攻撃者の言葉で示し、それに応える防御策を紹介します。
ステージ 1 / 3
標的を知る必要はない。パッケージを選べば、標的は勝手に決まる。
レッドチーム
相手のロックファイルは要らない。人気パッケージはほぼあらゆるものの推移的依存関係ツリーの奥深くにあり、ほとんどのチームは自分たちがどれを出荷しているか答えられない。公開されている手がかりで絞り込める。JavaScriptバンドルやHTTPヘッダーのバージョン、公開リポジトリ、コンテナイメージ。そして、postinstallスクリプトがダウンロードするバイナリやベースイメージの下のレイヤーなど、ロックファイルの外でビルドが取得するものは相手のスキャナーには見えていない。だから誰もパッチを当てていない。
ブルーチーム
CRACIはすべてのビルドに入ってくる通信を見て、ビルドが取り込むすべてのパッケージとファイルを記録します。ロックファイルベースのツールが見落とす隠れた依存関係も、推移的依存関係ツリーのあらゆる階層で記録します。SBOMは出荷したビルドから作成されるため、攻撃者が必要としなかった一覧を、誰かが探し始める前に手元に用意できます。
ビルド時SBOM →レッドチーム
新しいアドバイザリには、どのバージョンが脆弱かが正確に書いてある。エクスプロイトを作り、応答するものをスキャンし、スキャン結果に相手が誰かを教えてもらう。相手のチームがまだSlackのスレッドで「うちは影響を受ける?」と聞いている間に、こちらは当日から動き出せる。
ブルーチーム
CRACIは監視対象のSBOMを新しい脆弱性データと継続的に照合して再評価します。ビルド履歴から、脆弱な依存関係(直接・推移的を問わず)を含んでいたビルドがわかるため、それがどこに、どれだけの期間出荷されていたかを把握できます。
影響を受けるか? →ステージ 2 / 3
パッチが当たる前に欠陥を突くか、欠陥を使わずにパイプラインに便乗する。
レッドチーム
AIは、相手のセキュリティチームがチケットをトリアージする前に、アドバイザリとパッチの差分から実際に動く攻撃を作り出す。公開はスタートの合図だ。相手のスプリント計画は来週だ。
ブルーチーム
ポリシーゲートは、特定のCVEを含むビルドをブロックできます。そのため、依存関係ツリーのどれほど深くにあっても、判断したその瞬間から脆弱なバージョンの出荷を止められます。
CVEの修正 →レッドチーム
ワークフローは@v4のようなタグを信頼しているが、タグは移動できる。人気のGitHub Actionを1つ侵害すれば、それを使うすべてのワークフローが、シークレットに手が届く状態でこちらのコードを実行する。
ブルーチーム
CRACIは、ワークフローが実行するGitHub Actionsを、実際に実行されたコミットに固定した形でビルドのSBOMに記録します。Actionが侵害されたとき、タグから推測するのではなく、どのビルドが不正なコードを実行したかを確認できます。
CI/CDセキュリティチェックリスト →ステージ 3 / 3
シークレットを奪い、さらに次を狙って居座る。
レッドチーム
悪意あるパッケージのリリースは、トークンやクラウドの認証情報が置かれている相手のビルドの中でインストールスクリプトを実行する。CIランナーはたいていインターネット全体に接続できるので、パッケージが悪質だと誰かが気づく前に、シークレットはこちらのサーバーへ送られる。
ブルーチーム
エグレスポリシーをデフォルト拒否に設定すると、ビルドはパッケージソースと公開先にのみ到達でき、それ以外はすべてブロックされます。マルウェアがシークレットを攻撃者のホストに送信しようとしても、誰も見たことのない攻撃の最初の被害者になる場合でもブロックされ、ポリシー違反のアラートが届きます。
エグレスポリシー →レッドチーム
長期間稼働するセルフホストランナーは格好の獲物だ。ワークスペースやツールキャッシュに何かを仕込んで、次のジョブと、次のシークレットがやって来るのを待てばいい。
ブルーチーム
CRACIのジョブはすべて専用の分離された仮想マシンで実行されるため、あるジョブが残したものが次のジョブを待ち構えることはありません。パッチを当てて堅牢化しなければならないランナーのフリートもありません。
マネージドとセルフホストのランナー →公開後
攻撃者は、自社がその依存関係を使っていることを知る必要はありません。あらゆる場所でエクスプロイトを試し、何が応答するかを見るだけです。自社のチームは、その依存関係がどこに出荷されたかを知る必要があります。違いは、探しに行かなければならないかどうかです。そしてこの競争は毎年増えています。1日に公開されるCVEの数をご覧ください。
レッドチーム
ブルーチーム(ビルドの記録なし)
ブルーチーム(CRACIあり)
1つの対策ですべてのサプライチェーン攻撃を防げるわけではありません。これらを重ねることで、上記のどの段階でも攻撃が難しくなります。パイプライン側の詳細は、CI/CDパイプラインセキュリティチェックリストをご覧ください。
攻撃者はすべてのアドバイザリを監視し、脆弱性が公開されたその日に全員にエクスプロイトを試します。攻撃者には自社の依存関係の一覧は必要ありませんでした。必要なのは自社のほうです。CRACIは、ビルドそのものから得たその一覧をチームに提供し、攻撃者が当てにしている扉、つまり無制限のエグレスと共有ランナーを閉じます。
製品そのものではなく、オープンソースパッケージ、CI/CDコンポーネント、ビルドシステムなど、製品を構成するコード、ツール、サービスを狙う攻撃です。1回の侵害が、影響を受けたコンポーネントを使うすべての組織に及びます。
Shai-Huludワーム(2025年以降)は、感染したnpmパッケージを通じて拡散し、CIのシークレットを盗みました。tj-actions/changed-filesの侵害(CVE-2025-30066)では、人気のGitHub Actionが乗っ取られました。xz utilsのバックドア(CVE-2024-3094)は、上流の圧縮ライブラリに仕込まれました。Log4Shell(CVE-2021-44228)は、広く使われているJavaのログ記録ライブラリの欠陥を悪用しました。CodecovのBash Uploader(2021年)はCIの認証情報を外部サーバーに送信するよう改変され、SolarWinds Orion(2020年)はビルドで挿入された悪意あるコードを含んだまま出荷されました。
SolarWindsです。2020年、SolarWinds Orionのビルドプロセスにひそかにアクセスした攻撃者がリリースに悪意あるコードを挿入し、顧客はそれを通常のアップデートとしてインストールしました。この事件は、クリーンなソースツリーだけでは不十分であり、ビルドそのものが信頼でき、検証可能でなければならないことを示しました。
通常は知りませんし、知る必要もありません。ほとんどの攻撃は企業ではなくパッケージを狙います。アドバイザリが公開されると、攻撃者は脆弱なバージョンを実行している可能性のあるあらゆるものにエクスプロイトを試し、何が応答するかを確認します。JavaScriptバンドルやオープンソースのリポジトリに含まれるバージョンなど、公開されている手がかりが狙いを定める助けになります。非対称なのは、防御側には一覧が必要だという点です。影響を受けるかどうかを知るには、ロックファイルの外で取得したものも含め、すべてのビルドが何を出荷したかを把握している必要があります。
1つの対策ですべてを防げるわけではありません。すべてのビルドが出荷するもののSBOMを保持して新しいアドバイザリに対して監視し、依存関係とGitHub Actionsを固定し、脆弱なバージョンをビルドでブロックし、CIジョブの接続先を制限し、ジョブを分離してシークレットを制限し、デプロイした成果物が自社でビルドしたものであることを検証してください。
ロックファイルをコミットしてCIではそこからインストールし、ビルドに不要な場合はインストールスクリプトを無効にしてください(npmの--ignore-scripts)。各ビルドが実際にインストールしたパッケージのバージョンを記録しておけば、Shai-Huludのような新たな攻撃の波が発表されたときにすばやく確認できます。CIジョブの接続先を制限して、悪意あるインストールスクリプトがトークンを外部に送信できないようにし、感染したバージョンが到達できた可能性のあるトークンはすべてローテーションしてください。
セキュリティ演習では、レッドチームが攻撃者を演じ、ブルーチームが防御します。このページも同じ構成で、サプライチェーン攻撃の各段階を攻撃者の言葉で示し、それに防御側が応えます。
既知のものは検出します。xzのバックドアのように公開されたCVEがある悪意あるリリースは、他の脆弱性と同様にフラグが立てられ、ポリシーゲートでそれを含むビルドをブロックできます。CRACIはパッケージの挙動を解析したり評判をスコアリングしたりしないため、まったく新しい悪意あるパッケージは、アドバイザリが出るまでフラグが立ちません。そのためにあるのがエグレスポリシーです。デフォルト拒否であれば、ビルドは必要なホストにしか到達できないため、誰も見たことのない攻撃でも、パッケージがシークレットを外部に送信することはできません。