Skip to content

Back to the catalog

The Pwn Request Reckoning: What Still Works Against GitHub Actions
wiz.ioarticle

The Pwn Request Reckoning: What Still Works Against GitHub Actions

The Pwn Request Reckoning is a Wiz post updating the GitHub Actions pull_request_target threat model for 2026. It covers what GitHub's checkout protections, read-only cache tokens and event policies block, and what still works. Remaining paths: SHA-pinned pre-backport actions/checkout, custom checkout scripts, issue_comment and repository_dispatch triggers, script injection, artifact poisoning, self-hosted runner state and allow-unsafe-pr-checkout. It ends with grep and zizmor audit steps.

TL;DR Explains which pwn request paths in GitHub Actions survive GitHub's 2026 protections and how to audit and fix them.
Document

What it is

A Wiz post (Part 3 of its GitHub Actions research) that updates the pull_request_target threat model after GitHub's 2026 platform changes: checkout protections in actions/checkout, read-only Actions cache tokens, and event policies. Wiz says the changes make the default exploit harder but shift the vulnerability class rather than remove it. From December 2, pull_request_target stops running in public repositories unless an event policy allows it.

How it works

Each surviving path sits outside what GitHub's changes cover.

  • SHA pinning. The protections live inside actions/checkout, backported on July 20, 2026. Mutable tags such as @v4 (resolving to v4.4.x) and @v4.4.0 are protected. @v4.3.0, or a SHA pointing at it, runs legacy code with no checks. Wiz confirmed this by testing.
  • Custom checkout. git clone, gh pr checkout and gh repo clone fetch fork code with no protection. Third-party checkout actions use their own git logic and are also unprotected.
  • Other triggers. Checkout protections cover only pull_request_target and workflow_run. issue_comment bot commands (/deploy, /test, /cherry-pick) stay exploitable on checkout v7 without allow-unsafe-pr-checkout. repository_dispatch is exploitable when payload data chooses the checkout ref.
  • Script injection. ${{ github.event.pull_request.title }} inside run: is substituted before the shell runs. A title like "; touch /tmp/pwned; echo INJECTED # executes with the job's secrets. No checkout is involved.
  • Artifact poisoning. An unprivileged pull_request workflow uploads attacker-built output, and a privileged workflow_run downloads and executes it. Nothing marks artifacts from untrusted runs as tainted.
  • Self-hosted runners. On non-ephemeral runners, ~/.npm, ~/.cache/pip and build directories persist outside the cache service, so they can poison later release jobs.
  • Approval setting. "Require approval for all external contributors" gates only pull_request. pull_request_target runs immediately.

Preconditions / Affected

  • The December 2 default does not apply to private or internal repositories, or to workflow_run.
  • Workflows that maintainers explicitly allow through an event policy (labelers, preview deploys, fork CI with secrets) are the ones that handle untrusted input with privileges.
  • Anything using allow-unsafe-pr-checkout reopens the original vector.

What to do

Keep actions/checkout at or above the patched releases: v2.8.0, v3.7.0, v4.4.0, v5.1.0, v6.1.0 or v7.0.0+. For SHA-pinned refs, resolve each SHA to its release tag and compare. Wiz provides a script for this, but the page text does not include it.

Audit grep from the post, with each hit reviewed by hand (zizmor is the alternative):

# Flag risky triggers, unsafe checkouts, outdated checkout versions, artifact downloads,
# self-hosted runners and event data in expressions (env: usage is safe), then review each hit
grep -rnE 'pull_request_target|workflow_run|issue_comment|repository_dispatch|allow-unsafe-pr-checkout|self-hosted|download-artifact|gh (pr checkout|repo clone)|git (clone|fetch)|refs/pull|actions/checkout@v(1|2\.[0-7]|3\.[0-6]|4\.[0-3]|5\.0|6\.0)([^0-9]|$)|\$\{\{[[:space:]]*github\.(event\.|head_ref)' .github/workflows/

Fixes the post recommends:

  • Pass untrusted values through env: and reference them as a double-quoted "$PR_TITLE". Unquoted, the shell still word-splits and globs.
  • For issue_comment and repository_dispatch, check the commenter's real permission with getCollaboratorPermissionLevel (write or above), not author_association. Check out the exact head SHA and abort if the PR changed after the comment. Validate payload fields against an anchored allowlist regex.
  • Gate privileged pull_request_target jobs with an environment that requires reviewers, or with Workflow Execution Protections actor rules.
  • Use ephemeral runners for anything untrusted events can trigger, and separate runner groups for trusted and untrusted workflows.
  • Before December 2, review policy insights for pull_request_target runs that would be blocked. Move workflows that don't need it to pull_request. For the rest, write an event policy that allows specific workflow files only, and re-audit those files on every change.

Caveats

  • GitHub's changelog says further hardening of other events "may be explored in future releases," so the issue_comment and repository_dispatch gaps have no committed fix.
  • The post is from Wiz, which also sells Wiz Code for this scanning. The attack claims come from its own testing.

Organisations named here

  • allow-unsafe-pr-checkout
  • artifact poisoning
  • Wiz Research

Technologies named here

  • Wiz Code
  • Third-party checkout actions
  • grep audit command over .github/workflows/
  • Workflows that use the allow-unsafe-pr-checkout flag
  • actions/checkout v7
  • GitHub event policies and policy insights
  • Workflow Execution Protections actor rules
  • GitHub environments with required reviewers
  • Non-ephemeral self-hosted runners
  • getCollaboratorPermissionLevel
  • Private and internal repositories
  • GitHub Actions
  • GitHub
  • zizmor
  • Wiz

Techniques named here

  • Pwn request via pull_request_target
  • Exploiting unprotected privileged triggers
  • Artifact poisoning
  • SHA pinning paradox
  • Pwn Request
  • Script injection