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/checkout automatically refuses to check out fork PR code in pull_request_target and workflow_run workflows to prevent secret exfiltration.
  • Explicit opt-in: The allow-unsafe-pr-checkout input 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.ts and is invoked from src/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →