【GitHub Actions】pull_request_target の罠:TanStack事件から学ぶCI/CDサプライチェーン攻撃の手口と対策

CI/CD & DevOps

この記事の概要

2026年5月11日、Reactエコシステムで広く使われるTanStackライブラリが改ざんされ、OpenAIを含む多数の組織が影響を受けた。
根本原因は pull_request_target ワークフローの設定ミスによる「Pwn Request」攻撃だった。
自分のリポジトリが同じ罠にはまっていないか、今すぐ確認すべき具体的な方法を解説する。


CI/CDパイプラインこそが最大の攻撃面になった

GitHub Actionsを使ってCIを動かしているリポジトリは世界に何百万とある。
その多くが、気づかぬうちに「攻撃者にnpmパッケージを公開させてしまう穴」を抱えている。

2026年5月11日19:20〜19:26 UTC、わずか6分の間に42個の @tanstack/* npmパッケージに84バージョンの悪意あるコードが公開された。
マルウェアは npm install を実行した開発者・CIマシンからAWSクレデンシャル、GCPメタデータ、Kubernetesトークン、GitHub/npmトークン、SSHキーを片っ端から盗み出した。

被害は「ライブラリ利用者」にとどまらない。
攻撃者が狙ったのはnpmパッケージのメンテナーであり、乗っ取られた認証情報を使って同様の攻撃を連鎖させる自己伝播型マルウェアが仕込まれていた。

なぜこんなことが起きたのか。
そして自分のリポジトリは安全か。
インフラ・DevOpsエンジニア向けに技術的な背景から実践的な対策まで解説する。


なぜ pull_request_target は危険なのか

GitHub Actionsには似た名前のトリガーが2つある。

# ① フォークPRのコードを「信頼しない」トリガー
on:
  pull_request:

# ② フォークPRでも「ベースリポジトリの権限で動く」トリガー
on:
  pull_request_target:

pull_request はフォークからのPRに対してリポジトリのシークレットやwriteトークンを渡さない。
安全な設計だ。

一方 pull_request_target は「PRのコメントを書き込む」「ラベルを付ける」といった、ベースリポジトリへの書き込みが必要な操作のために設計されたトリガーで、ベースリポジトリの権限で動く
初回コントリビューターの承認ゲートも pull_request_target には適用されない。

つまり「フォークPRのコードを pull_request_target ワークフロー内で実行する」という組み合わせが成立した瞬間、外部の攻撃者がベースリポジトリの権限で任意コードを実行できる状態になる。
これを Pwn Request と呼ぶ。

TanStackの bundle-size.yml はまさにこのパターンにはまっていた。

# 脆弱なワークフローの例(TanStack bundle-size.ymlを簡略化)
on:
  pull_request_target:
    paths: ['packages/**']

jobs:
  benchmark-pr:
    steps:
      - uses: actions/checkout@v6.0.2
        with:
          ref: refs/pull/${{ github.event.pull_request.number }}/merge
          # ↑ フォーク側のコードをチェックアウト

      - run: pnpm nx run @benchmarks/bundle-size:build
          # ↑ フォーク側のコードを実行 ← ここが爆弾

ワークフロー作者はコメントで「benchmark-pr は untrusted、権限は read-only にした」と意図を書いていた。
しかし actions/cache@v5キャッシュ書き込みは権限設定の外にあるという事実を見落としていた。


3つの脆弱性が連鎖した攻撃の全貌

攻撃は3段階の脆弱性を巧みに組み合わせて成立した。
それぞれ単独では致命的ではないが、組み合わさることで完全な「サプライチェーン乗っ取り」が実現した。

flowchart LR
    A[攻撃者がフォークPRを送付\npull_request_target を悪用] --> B[ベンチマークジョブが\nフォークコードを実行]
    B --> C[悪意あるコードが\nGitHub Actionsキャッシュを汚染\n約8時間前に仕込む]
    C --> D[mainブランチへのpush後\nrelease.ymlが汚染キャッシュを復元]
    D --> E[OIDCトークンをrunnerメモリから抽出]
    E --> F[npmレジストリに84バージョンを公開]
    F --> G[マルウェアが開発者・CI環境の\nクレデンシャルを窃取]

ステップ1:Pwn Request でフォークコードを実行させる

攻撃者は2026年5月10日にTanStack/routerのフォーク(zblgg/configurationという紛らわしい名前)を作成。
[skip ci] で自身へのCI実行を抑制した悪意あるコミットを仕込んだPRを送った。
pull_request_target トリガーは初回コントリビューターのゲートをスキップし、ベンチマークジョブが自動実行された。

ステップ2:キャッシュを汚染して8時間待つ

フォークコードの実行により、攻撃者は pnpm-store ディレクトリを改ざんした。
actions/cache@v5 はpost-jobで汚染されたキャッシュを本番のmainブランチと同じスコープに保存した。
キャッシュキーは pnpm-lock.yaml のハッシュを使うため、次にmainブランチでリリースワークフローが動いたときに確実に復元される。

ステップ3:OIDC トークンをrunnerメモリから抜き取る

release.yml は npmへのOIDC信頼発行者(Trusted Publisher)として設定されており、id-token: write 権限を持っていた。
汚染されたキャッシュが復元された後、悪意あるバイナリが /proc/<pid>/mem からGitHub Actions runnerのメモリを直接読み取り、OIDCトークンを抽出。
そのトークンを使ってnpmレジストリに直接PUBLISHした。

攻撃者はワークフローの「Publish Packages」ステップを経由していない。
テストが失敗してそのステップはスキップされたにもかかわらず、マルウェアは別のコードパスから直接npmに公開した。


実際に何が盗まれるのか:マルウェアの動作詳細

影響を受けたバージョンを npm install した開発者・CIマシンでは以下が実行された。

# マルウェアが収集するクレデンシャルの一覧(概要)
~/.aws/credentials         # AWSアクセスキー
~/.config/gcloud/          # GCPサービスアカウント
~/.kube/config             # Kubernetesトークン
~/.vault-token             # HashiCorp Vaultトークン
~/.npmrc                   # npmパブリッシュトークン
~/.gitconfig + gh CLIトークン
~/.ssh/id_*                # SSH秘密鍵
.env ファイル               # 各種APIキー

さらに恐ろしいのは 自己伝播ロジック だ。
窃取したnpmトークンを使って、被害者が管理するすべてのパッケージを同じマルウェアで上書きしようとする。
1台のCIマシンの感染が組織全体のnpmパッケージ汚染につながる。

加えてマルウェアは gh-token-monitor という永続デーモンをインストールし、トークンが失効するとホームディレクトリに rm -rf を実行するワイパー機能まで持っていた。


今すぐ確認すべき:自分のリポジトリは安全か

脆弱なパターンを検出する

自分のリポジトリで以下のコマンドを実行して、Pwn Requestの可能性があるワークフローを探す。

# pull_request_target を使っているワークフローを検索
grep -rn "pull_request_target" .github/workflows/

# checkout で refs/pull を使っているワークフローを検索
grep -rn "refs/pull" .github/workflows/

両方にヒットするワークフローは要注意だ。
pull_request_targetrefs/pull/*/merge をチェックアウトして何かを実行している」パターンが最も危険。

安全な書き方:トリガーとコード実行を分離する

# 安全なパターン:信頼できる操作だけを pull_request_target で行う
on:
  pull_request_target:

jobs:
  label:
    # ラベル付けなど、外部コードを実行しない操作のみ
    steps:
      - name: Add label
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.addLabels({...})
            # ↑ コードの実行なし。これは安全

---

# フォークコードを実行する必要があるなら pull_request を使う
on:
  pull_request:

jobs:
  build:
    steps:
      - uses: actions/checkout@v4
      # pull_request はシークレットなし・権限なしで動く。安全。

キャッシュの隔離を徹底する

# キャッシュキーにブランチ情報を入れてフォークとmainを分離する
- uses: actions/cache@v4
  with:
    key: ${{ runner.os }}-pnpm-${{ github.ref }}-${{ hashFiles('**/pnpm-lock.yaml') }}
    #                              ↑ github.ref を入れることで
    #                              フォークPRとmainブランチのキャッシュが混在しない

OIDC Trusted Publisher の範囲を絞る

npmのOIDC信頼発行者を設定している場合、特定のジョブID(environment)に限定することで、想定外のコードパスからのpublishを防げる。

# npm trusted publisher 設定時のベストプラクティス
jobs:
  publish:
    environment: npm-publish  # ← 本番公開専用 environment を作成
    permissions:
      id-token: write
    steps:
      - run: npm publish --provenance

environment に設定した場合、そのジョブ以外では id-token が発行されないため、別のコードパスからマルウェアがOIDCトークンを取得することを防げる。


今すぐ CI/CDパイプラインの守りを固めよう

TanStack事件は「有名OSSだから安全」という思い込みを打ち砕いた。
攻撃者が使った手口は2024〜2025年にすでに公開された既知の脆弱性の組み合わせに過ぎない。
つまり、対策さえしていれば防げた攻撃だ。

対策の優先順位は次の通りだ。

  1. ワークフロー監査: pull_request_target + コード実行の組み合わせをリポジトリ全体で探す
  2. Actionをコミットハッシュで固定: タグ指定をSHAに変える(例: actions/checkout@11bd71901bbe...
  3. StepSecurity Harden-Runner導入: ランタイムの不審なネットワーク接続を検出(TanStack事件の発見者も使用)
  4. npm OIDC の environment 指定: 公開専用ジョブ以外でトークンが発行されないよう制御する
  5. npm audit signatures: 公開済みパッケージの署名を定期検証する

「小さいリポジトリは狙われない」は通用しない。
今すぐワークフローを見直してほしい。


まとめ

  • pull_request_target はベースリポジトリ権限で動くため、フォークコードの実行と組み合わせると致命的な脆弱性になる
  • GitHub Actionsキャッシュはブランチスコープが共有されており、フォークPRからmainブランチのキャッシュを汚染できる
  • OIDC Trusted Publisherは environment で範囲を絞らないと任意のコードパスから使われる
  • 対策はシンプルで、既知の手法で防げる問題。監査と修正を今日中に始めよう

参考リンク

コメント