開発効率化

actions/checkout v7入門——pull_request_targetの危険なcheckoutを止める

GitHub Actionsのactions/checkout v7を題材に、pull_request_targetで未信頼コードを特権付きで実行しないCI設計を、Next.jsとFirestoreの監査例で解説します。

2026年5月13日
GitHub ActionsCI/CDセキュリティNext.jsFirestore
actions/checkout v7入門——pull_request_targetの危険なcheckoutを止める

はじめに

GitHub ActionsでPR自動化を作っていて、pull_request_targetの中でfork PRのhead SHAをcheckoutしていませんか?このイベントはbase repositoryのGITHUB_TOKENやsecretsを使えるため、レビュー前のコードを特権付きrunnerで実行すると事故につながります。

2026年6月18日、GitHubは actions/checkout v7 を一般提供し、pull_request_targetや一部のworkflow_runでfork PRのコードを危険な形でcheckoutするパターンをデフォルトで拒否するようにしました。GitHubはこの種の事故を「pwn request」と呼び、サプライチェーン攻撃の原因になってきたと説明しています。

本記事では、架空の請求管理SaaS「BillPort」を題材に、Next.js + Firestore + GitHub Actions + TypeScriptの現場で、PR自動化を止めずに安全側へ寄せる手順を整理します。

何が変わったのか

pull_request_targetは、PR元ではなくbase repository側のworkflow定義で動くイベントです。ラベル付けやコメント投稿のようにPRへ書き戻したい場面では便利ですが、base repositoryのGITHUB_TOKEN、secrets、default branchのcacheなどにアクセスできるため、fork PRの未信頼コードをそのまま実行すると危険です。

actions/checkout v7では、fork PRから来たコードに対して、次のような入力を使う危険パターンが拒否されます。

入力例なぜ危険か
ref: ${{ github.event.pull_request.head.sha }}fork側の未レビューcommitを特権付きjobに取り込む
ref: refs/pull/${{ github.event.pull_request.number }}/mergePRのmerge refを特権付きcontextで実行する
repository: ${{ github.event.pull_request.head.repo.full_name }}fork repositoryそのものをcheckoutする

GitHubの発表では、2026年7月16日にサポート中のmajor versionへもこの保護をbackportするとされています。actions/checkout@v4のような浮動major tagは自動的に影響を受けますが、SHAやminorへ固定しているworkflowは自分たちで更新が必要です。

ハンズオン1: 危険なworkflowを探す

まず、BillPortの.github/workflowsから危険候補を洗い出します。workflow_runissue_commentからPRコードを手動で取りに行く処理も見ます。

rg "pull_request_target|workflow_run|issue_comment|pull_request.head|refs/pull|allow-unsafe-pr-checkout" .github/workflows

見つかったworkflowは、次の表で分類します。

分類対応
PRメタデータだけを読むpull_request_targetのままでもよいがcheckoutしない
base branchのコードだけ使うactions/checkout@v7で通常checkoutする
fork PRのコードを実行するpull_requestへ移す
secretsも必要環境を分離し、明示的なレビュー後に実行する

actions/checkout v7は万能ではありません。runブロック内でgit fetchgh pr checkoutを使ってPR headを取得する処理は、checkout actionの保護範囲外としてレビューします。

ハンズオン2: PR検証とPR操作を分ける

BillPortでは、Next.jsのビルドとFirestore Emulatorを使うテストはpull_requestで実行し、PRへのコメント投稿だけをpull_request_targetへ残します。

name: PR Quality Gate

on:
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "yarn"

      - run: yarn install --frozen-lockfile
      - run: yarn build

PRへコメントするworkflowは、base repositoryのworkflowだけをcheckoutし、PR headはcheckoutしません。

name: PR Metadata Comment

on:
  pull_request_target:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write

jobs:
  comment:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: echo "PRメタデータだけを扱い、PR headはcheckoutしない"

この分離により、未レビューのコードを実行するjobにはsecretsや書き込み権限を渡さず、書き込み権限が必要なjobでは未レビューコードを実行しない、という設計になります。

ハンズオン3: Firestoreへ監査結果を保存する

CIの安全性は一度直して終わりではありません。AIエージェントやbotがworkflowを追加する運用では、危険な入力が戻ってこないかを継続的に見ます。BillPortでは、監査結果をFirestoreのciWorkflowAuditsへ保存します。

// src/app/api/internal/ci-workflow-audits/route.ts
import { initializeApp, getApps } from "firebase-admin/app";
import { FieldValue, getFirestore } from "firebase-admin/firestore";
import { NextResponse } from "next/server";

if (getApps().length === 0) initializeApp();

const db = getFirestore();

type WorkflowFinding = {
  file: string;
  risk: "pull_request_target" | "unsafe_pr_ref" | "manual_git_fetch";
  line: string;
};

export async function POST(request: Request) {
  const { sha, findings } = (await request.json()) as {
    sha: string;
    findings: WorkflowFinding[];
  };

  if (!sha || !Array.isArray(findings)) {
    return NextResponse.json({ error: "invalid payload" }, { status: 400 });
  }

  await db.collection("ciWorkflowAudits").add({
    sha,
    findings,
    findingCount: findings.length,
    createdAt: FieldValue.serverTimestamp(),
  });

  return NextResponse.json({ ok: true });
}

監査jobは、まずテキスト検出で十分です。

name: Audit workflow checkout safety

on:
  schedule:
    - cron: "0 23 * * *" # 08:00 JST
  workflow_dispatch:

permissions:
  contents: read

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - name: Detect risky workflow patterns
        run: |
          rg -n "pull_request_target|pull_request.head|refs/pull|gh pr checkout|git fetch" .github/workflows || true

      - name: Fail when unsafe opt-out is present
        run: |
          if rg "allow-unsafe-pr-checkout:\\s*true" .github/workflows; then
            echo "allow-unsafe-pr-checkout requires security review"
            exit 1
          fi

allow-unsafe-pr-checkout: trueは実在するactions/checkout v7の入力ですが、名前の通り危険を理解した上で使うための明示的なopt-outです。使う場合は、runner network、キャッシュ、成果物の扱いまでレビュー対象にします。

運用で見るべきポイント

actions/checkout v7への更新は、単なるバージョン上げではありません。CIを次の3層に分けるきっかけにすると効果が出ます。

権限
未信頼コード検証pull_requestyarn build、型チェック、テストcontents: read中心
PR操作ラベル付け、コメント、変更ファイル一覧の確認pull-requests: writeなど必要最小限
デプロイ・外部連携Firebase Hosting、Firestore本番、外部APIprotected environmentと承認を必須にする

Next.js + Firestoreのプロジェクトでは、PR検証で本番Firestoreに接続しない設計も重要です。Firestore RulesやEmulatorを使うテストはPR側で実行し、本番データを使う確認はworkflow_dispatchとprotected environmentに分離します。

参考リンク

まとめ

actions/checkout v7は、pull_request_targetの典型的な危険checkoutをデフォルトで止める重要な更新です。ただし、保護されるのはcheckout actionで検出できる範囲に限られます。

実務では、PRのコードを検証するworkflowと、PRへ書き戻すworkflowを分けることが基本です。さらにFirestoreへ監査結果を残しておくと、AIエージェントやbotがworkflowを増やすチームでも、危険な設定が戻ってきたタイミングを追跡できます。まずは.github/workflowsを棚卸しし、actions/checkout@v7へ上げるところから始めましょう。