Security

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

Vulnerable workflow (DO NOT COPY)
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 }}
Safer pattern — split workflows
# .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 page

FAQs

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