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.
# .github/workflows/lint.yml — ❌ Head checkout in privileged triggeron: pull_request_targetjobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # ← attacker code - run: ./lint.sh# .github/workflows/lint.yml — ✅ Use pull_request, no head checkout neededon: pull_requestpermissions: contents: readjobs: 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).
- 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: trueSee the CLI documentation for the full configuration reference.