How actions/checkout Prevents Pwn Request Attacks in pull_request_target Workflows
The actions/checkout action blocks checkout of fork pull request code in privileged pull_request_target workflows unless explicitly enabled via allow-unsafe-pr-checkout, preventing attackers from exfiltrating base repository secrets.
The actions/checkout action implements a critical security guard to mitigate "pwn request" vulnerabilities when handling the pull_request_target event. This event executes workflows with the base repository's GITHUB_TOKEN, secrets, and full runner access, creating a high-risk attack surface if malicious fork code is allowed to run. By default, the action refuses to check out code from forked pull requests in this privileged context, requiring explicit opt-in only after security review.
Understanding the Pwn Request Vulnerability
A "pwn request" attack occurs when a malicious actor submits a pull request from a forked repository containing exploitable code. When a workflow triggers on pull_request_target, GitHub executes that workflow with the base repository's permissions—including access to secrets, the GITHUB_TOKEN with write permissions, and the default branch cache scope.
If the workflow checks out and executes code from the forked PR in this privileged context, the attacker can exfiltrate secrets, modify the base repository, or compromise the runner. This vulnerability specifically affects pull_request_target and workflow_run events because they run with elevated privileges compared to standard pull_request events.
The Safety Guard: assertSafePrCheckout
The core protection logic resides in src/unsafe-pr-checkout-helper.ts within the assertSafePrCheckout function. This guard implements an eight-step validation process that halts execution when it detects potentially dangerous checkout attempts.
Step 1: Opt-Out Validation
The guard first checks for explicit user consent to bypass security checks.
if (input.allowUnsafePrCheckout) return
If the allow-unsafe-pr-checkout input is set to true, the function returns immediately, permitting the checkout to proceed. This allows advanced users to accept the risk after reviewing the security implications.
Step 2: Event Filtering
The protection only applies to high-risk workflow triggers.
const eventName = github.context.eventName
if (eventName !== 'pull_request_target' && eventName !== 'workflow_run') return
Standard pull_request events run with the fork's own limited token, so they do not require this guard. The check specifically targets pull_request_target and workflow_run events that execute with base repository privileges.
Step 3: Repository Identification
The function extracts repository identifiers from the GitHub webhook payload to distinguish between the base repository and potential forks.
const baseRepoId = fromPayload('repository.id')
// ...
prHeadRepoId = fromPayload('pull_request.head.repo.id')
prHeadRepoFullName = fromPayload('pull_request.head.repo.full_name')
This retrieves the base repository ID and the fork's repository ID for comparison.
Step 4: Fork Detection
The guard determines if the PR originates from a fork by comparing repository IDs.
if (typeof prHeadRepoId !== 'number' || prHeadRepoId === baseRepoId) return
If the head repository ID matches the base repository ID, the PR originates from an internal branch rather than a fork, making the checkout safe.
Step 5: Intent Verification
Before blocking, the guard verifies whether the checkout request actually targets the fork's code through three validation checks:
const repositoryMatchesPrHead =
typeof prHeadRepoFullName === 'string' &&
input.qualifiedRepository.toLowerCase() === prHeadRepoFullName.toLowerCase()
const refMatchesPullPattern = PR_REF_PATTERN.test(input.ref)
const commitMatchesPrHeadSha = !!input.commit && prShas.includes(input.commit.toLowerCase())
- Repository match: The checkout targets the fork's repository name specifically
- Ref pattern match: The ref follows the pull request pattern (
refs/pull/...) - SHA match: The commit SHA exists in the fork's head or merge commit SHAs
If none of these conditions match, the function returns without error, assuming the checkout targets unrelated code.
Step 6: Security Exception
When the guard detects a fork PR checkout attempt without explicit opt-in, it throws a descriptive error:
throw new Error(
`Refusing to check out fork pull request code from a '${eventName}' workflow. ` +
`This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch ` +
`cache scope, and runner access. Fetching and executing a fork's code in that trusted ` +
`context commonly leads to "pwn request" vulnerabilities. To opt in, review the risks ` +
`at https://gh.io/securely-using-pull_request_target and set 'allow-unsafe-pr-checkout: true' ` +
`on the actions/checkout step.`
)
This error message references the official GitHub security guide and explains the specific risks involved.
Configuration and Usage
Default Safe Behavior
By default, workflows using pull_request_target will fail when attempting to checkout fork PRs:
# .github/workflows/pr-target.yml
on:
pull_request_target:
types: [opened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: echo "This runs only for internal PRs"
When triggered by a forked PR, this configuration produces the security error and halts execution.
Explicit Opt-In for Unsafe Checkout
After reviewing the security risks at https://gh.io/securely-using-pull_request_target, enable fork PR checkout with explicit consent:
on:
pull_request_target:
types: [opened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
allow-unsafe-pr-checkout: true
- run: echo "Now we can run on fork PRs"
Safe Checkout Using Verified Fork SHA
Checkout specific fork commits without enabling unsafe mode by referencing the exact SHA from the event payload:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
This satisfies the guard's intent verification because the SHA matches the fork's head commit extracted from the webhook payload.
Key Implementation Files
The pwn request protection spans several files in the actions/checkout repository:
| File | Purpose |
|---|---|
src/unsafe-pr-checkout-helper.ts |
Implements the assertSafePrCheckout function containing the security logic |
src/main.ts |
Invokes the safety guard before executing the actual Git checkout operations |
action.yml |
Declares the allow-unsafe-pr-checkout input parameter and its default value of false |
README.md |
Documents the security behavior and links to the pull_request_target security guide |
Summary
- Default protection:
actions/checkoutautomatically refuses to check out fork PR code inpull_request_targetandworkflow_runworkflows to prevent secret exfiltration. - Explicit opt-in: The
allow-unsafe-pr-checkoutinput bypasses the guard only after user acknowledgment of the security risks. - Smart validation: The guard verifies repository IDs, ref patterns, and commit SHAs to distinguish between intentional fork checkouts and potentially malicious attempts.
- Clear error messages: Failed attempts provide specific guidance linking to GitHub's secure usage documentation.
- Source location: The protection logic resides in
src/unsafe-pr-checkout-helper.tsand is invoked fromsrc/main.ts.
Frequently Asked Questions
What is a pwn request attack?
A pwn request attack occurs when a malicious actor submits a pull request from a forked repository that exploits the privileged context of pull_request_target workflows. Because these workflows run with the base repository's GITHUB_TOKEN and secrets, executing untrusted fork code allows attackers to steal secrets, modify the repository, or compromise the CI runner.
Why does pull_request_target create security risks?
The pull_request_target event runs workflows in the context of the base repository rather than the fork, granting access to sensitive credentials and write permissions that standard pull_request events do not have. If the workflow checks out and runs code from the forked PR, it executes untrusted code with trusted privileges, creating the "pwn request" vulnerability.
How can I safely checkout fork PRs without enabling unsafe mode?
Use the specific commit SHA from the pull request event payload: ref: ${{ github.event.pull_request.head.sha }}. This satisfies the guard's intent verification because the SHA matches the fork's head commit identified in the webhook payload, proving the checkout specifically targets the fork's code rather than tricking the workflow into checking out unrelated malicious code.
What happens if I don't set allow-unsafe-pr-checkout?
Without allow-unsafe-pr-checkout: true, the workflow fails with an error message explaining the pwn request risk when actions/checkout detects a fork PR checkout attempt in a pull_request_target workflow. The error directs you to GitHub's security documentation and explains that the workflow runs with the base repository's secrets and token.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →