Skip to main content
ISSUE-804CriticalQuickCLIWorkflow triggers and permissions

pull_request_target workflow checks out the PR head

Control: pull_request_target must not check out the PR head· Config key: pullRequestTargetMustNotCheckoutHead

📋 What is this?

A workflow on the pull_request_target trigger explicitly checks out github.event.pull_request.head.sha (or head_ref). The job has access to the base repository's secrets and runs the PR author's code.

⚠️ Impact

This is the exact pattern behind the March 2025 tj-actions / reviewdog compromise. Any shell step that runs after the checkout is a direct path to secret exfiltration: npm install (runs package scripts), pytest (loads conftest.py), even cat README.md (via attacker-supplied content).

🔧 How to fix

Remove the explicit head checkout. pull_request_target should only check out the base branch (the default), where the code under review has already been merged.

✗ BeforeThe PR author's code now runs with `pull_request_target`'s secret-bearing GITHUB_TOKEN.
# .github/workflows/lint.yml: ❌ Head checkout in privileged trigger
on: pull_request_target
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # ← attacker code
- run: ./lint.sh
✓ After`pull_request` runs without secret access on fork PRs, so the head checkout is safe.
# .github/workflows/lint.yml: ✅ Use pull_request, no head checkout needed
on: pull_request
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# default ref: github.event.pull_request.head.sha is fine on pull_request
- run: ./lint.sh

💡 Tips

  • If a workflow truly needs both PR code AND secrets, the right pattern is: PR workflow uploads diff artefact → trusted workflow_run consumes it (no checkout).
  • A job-level same-repository if: guard (e.g. github.event.pull_request.head.repo.full_name == github.repository, head.repo.fork == false) silences the finding: fork-controlled code never reaches the checkout.
  • Plumber's default-on actionsMustBePinnedByCommitSha + ISSUE-802 + this rule cover the tj-actions class of vulnerabilities end-to-end.

⚙️ Configuration

This control is configured in .plumber.yaml under the github section:

github:
  controls:
    pullRequestTargetMustNotCheckoutHead:
      enabled: true

See the CLI documentation for the full configuration reference.