pull_request_target attack
A pull_request_target attack abuses GitHub Actions workflows that combine the pull_request_target trigger (runs with base-repo secrets) with checking out fork-controlled code, giving an attacker's fork PR access to your secrets.
The setup: pull_request_target vs pull_request
GitHub Actions has two PR-triggered workflow events: pull_request (runs with NO secrets when triggered from a fork) and pull_request_target (runs with the BASE repo's secrets, even when triggered from a fork).
pull_request_target was added so maintainers could run things like 'add a label' or 'post a welcome comment' on fork PRs — operations that need the base repo's GITHUB_TOKEN but don't need to execute fork code.
The key invariant: pull_request_target should NEVER check out fork code. The base repo's code is safe to run with secrets; the fork's code is not. Mix the two and you have an exploit.
The exploit pattern
An attacker forks the target repo and opens a PR. The PR modifies a file — could be a config, a test, a build script, anything that gets executed.
The base repo's workflow uses pull_request_target AND checks out the fork's head SHA (actions/checkout with ref: ${{ github.event.pull_request.head.sha }}).
When the workflow runs, it has the base repo's secrets in env AND it executes attacker-controlled code. The attacker exfiltrates GITHUB_TOKEN, AWS keys, npm publish tokens, anything in env.
The attack got a name — 'pwnrequest' — after a 2020 disclosure by Felix Wilhelm. Subsequent variants hit several major repos.
Why this keeps happening
The fix sounds obvious — don't check out fork code in pull_request_target workflows. But maintainers add this combination for legitimate-looking reasons:
Running tests against the PR's changes: 'we want CI to verify the fork PR works.' Tempting, broken — that's what pull_request (without secrets) is for.
Linting / formatting checks on the diff: 'we want to comment on style issues.' Use pull_request, or use pull_request_target without checkout (just read the diff from the API).
Building documentation previews: 'we want to deploy a Netlify preview of the fork branch.' Use a deploy service that doesn't need your secrets, or split the build into two stages.
Every one of these has a safer pattern; the attack keeps surfacing because maintainers reach for the convenient (and dangerous) combination first.
Detection: how an attack is recognised in workflow YAML
A static rule can catch this. The pattern: a workflow with `on: pull_request_target` AND a step that uses `actions/checkout` with `ref: ${{ github.event.pull_request.head.sha }}` or `ref: ${{ github.event.pull_request.head.ref }}`.
LGTM Security flags this combination at PR review time. The rule id is workflow.pull-request-target-checkout-pr-code — see the LGTM Security product page for the full enforcement gates (inline PR comment, blocking Check Run, runtime watchdog).
Other CI security tools that catch the same pattern: GitHub's own CodeQL action, OSSF Scorecard's Dangerous-Workflow check, custom rules in tools like Conftest. Defense in depth applies here — multiple detectors don't hurt.
How to fix the pattern safely
Option A — Split into two workflows. A 'pull_request' workflow (no secrets, runs fork code) handles the tests + lint. A separate 'pull_request_target' workflow (with secrets, runs only base code) handles the label/comment.
Option B — Read the PR head SHA without checking it out. If you only need to read the diff (e.g. for a security review), use the GitHub REST API from base-repo code instead of actions/checkout.
Option C — Gate by user trust. The pull_request_target workflow can check github.event.pull_request.user.login against a maintainers list, only running on PRs from trusted authors. Brittle — but workable for closed orgs.
Option D — Don't run on forks at all. Set if: github.event.pull_request.head.repo.full_name == github.repository to limit the workflow to same-repo PRs.
Examples
name: PR Validation
on: pull_request_target
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# ❌ DANGER — fork code runs with base repo's secrets
ref: ${{ github.event.pull_request.head.sha }}
- run: npm test
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}# .github/workflows/fork-tests.yml — no secrets
name: Fork Tests
on: pull_request # default — no secrets on fork PRs
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # safe — secrets not available
- run: npm test
# .github/workflows/label.yml — runs in base repo only
name: Label
on: pull_request_target
jobs:
label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/labeler@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}See LGTM Security's pull_request_target detector
16 detectors · merge-block Check Run · runtime watchdog
Go to the product pageFAQs
Does this only affect public repos?
Public repos are the highest-risk because anyone can fork and open a PR. But private repos with external collaborators have the same exposure if those collaborators can open PRs from forks. The attack surface scales with how many people you trust to open PRs.
What's the difference between pull_request_target and pull_request?
pull_request runs with NO repository secrets when triggered from a fork — designed to be safe to run fork code in. pull_request_target runs with the BASE repository's secrets — designed for trusted automation on the PR metadata, not the PR code.
Can I tell if my repo is currently vulnerable?
Yes. Grep your .github/workflows/ directory for files containing both 'pull_request_target' AND 'actions/checkout' with a 'ref:' pointing to head.sha or head.ref. If any workflow has both, it's vulnerable. LGTM Security's detector will flag this automatically once you enroll the repo.
What's the worst-case if I'm vulnerable?
Worst case: an attacker exfiltrates every secret your workflow has access to — typically including GITHUB_TOKEN (with write scope on the repo), cloud-provider credentials, npm/PyPI publish tokens. Real-world incidents have led to supply-chain compromises affecting downstream consumers of the breached package.
Is this still a problem in 2026?
Yes. The combination of pull_request_target + checkout-head still ships in new workflows monthly. The pattern is convenient and the warning isn't visible enough in the default GitHub UI. Detector-based tooling that flags it at PR time remains the most reliable mitigation.
Related across LGTM
Related terms
Self-hosted runner abuse
Self-hosted runner abuse: an attacker forks a public repo using a self-hosted GitHub Actions runner, opens a PR, and gets remote code execution on the runner's host — often the maintainer's own infrastructure.
Pinned GitHub Action (SHA vs tag)
A pinned GitHub Action references a specific commit SHA rather than a mutable tag (like v3) or branch. Pinning to a SHA means the action's source code can't change underneath you — a key supply-chain defense.
GitHub App permissions
GitHub App permissions are the granular scopes an installed GitHub App requests on a repository: contents (read source), pull requests (read+write), checks (write Check Runs), metadata (read repo info), etc. Each permission level (none / read / write / admin) controls what the App can do with that resource.
CI/CD security
CI/CD security covers the attack surface introduced by your build and deploy pipelines: GitHub Actions workflows, Dockerfiles, IaC configs, dependency management, and secret handling. Distinct from (and complementary to) application-layer AppSec.
Software supply-chain attack
A software supply-chain attack compromises a software product by attacking something earlier in the build/distribute chain: a dependency, a build tool, a package registry, a CI/CD pipeline, or a maintainer account. The downstream consumer ships compromised code without knowing.