← Actions Injection Check

What is a "pwn request" in GitHub Actions?

pull_request, the ordinary PR trigger, runs with a read-only token and no repository secrets — safe even against a hostile fork, because there is nothing valuable for the fork's code to reach. pull_request_target exists for the cases that need more: commenting on the PR, labeling it, or building it with real credentials. It runs in the context of the base repository, with the base repository's secrets and a privileged GITHUB_TOKEN — but the event that triggers it is still raised by whoever opened the pull request, who might be a stranger.

By itself that is fine: actions/checkout under pull_request_target checks out the base branch by default, not the fork. The danger is one line away:

- uses: actions/checkout@v4
  with:
    ref: ${{ github.event.pull_request.head.sha }}

That explicit ref: pulls the fork's own commit into the checkout — a natural thing to write if the goal is "build and test the PR's actual changes". If a later step runs npm install, a Makefile, or anything else from that checkout, it runs the fork's code with the base repository's secrets. workflow_run has the identical shape: it runs privileged, in response to a workflow that a fork's push already triggered.

Why is it called a "pwn request"?

Coined by security researchers describing exactly this pattern: the pull request itself is the exploit delivery mechanism. An attacker needs no special access — opening a pull request is the interaction the workflow is designed to respond to. GitHub's own security guidance ("Keeping your GitHub Actions and workflows secure, Part 1: Preventing pwn requests") documents the shape and the fix at length, and it was significant enough that actions/checkout v7 (June 2026) started refusing to fetch fork PR code in pull_request_target and workflow_run workflows by default.

The fix

Check your own workflows

Paste your workflow YAML into Actions Injection Check to find every pull_request_target/workflow_run checkout of an untrusted ref across every workflow file at once, or run the CLI in your own repository:

python3 check.py --fail-on-risk

That exits with a nonzero status if any workflow has this pattern (or the related script-injection pattern) — wire it into your own CI to block a newly-added one before it merges.