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.
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.
pull_request for anything that only needs to build and test the PR's code — it is unprivileged by design, so there is nothing to steal.pull_request_target's privileges (posting a comment, applying a label), do not check out the fork's code in that same job. Split the work: an unprivileged pull_request workflow builds and tests, uploads its results as an artifact; a separate pull_request_target job only downloads and acts on that artifact, never the fork's source.actions/checkout v7 or later, which refuses the common form of this pattern by default.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.