{"slug": "the-pwn-request-reckoning-what-still-works-against-github-actions", "title": "The Pwn Request Reckoning: What Still Works Against GitHub Actions", "summary": "GitHub's 2026 platform changes to GitHub Actions make the default-case pull_request_target exploit significantly harder but do not eliminate the vulnerability class, according to a security research post updating a three-part GitHub Actions threat model. GitHub released actions/checkout@v7.0.0 on June 18, 2026, blocking fork code checkouts by default in untrusted triggers, added read-only cache tokens on June 26, 2026, backported fork checkout protections on July 20, 2026 to actions/checkout v2.8.0, v3.7.0, v4.4.0, v5.1.0 and v6.1.0 (v1 excluded), and reached GA on cache-mode on September 10, 2026 and Workflow Execution Protections on September 17, 2026. GitHub is scheduled on December 2, 2026 to enforce a default event policy blocking pull_request_target in public repositories without an explicit event policy, a rule in evaluate mode since September 17 that does not apply to private or internal repositories or to workflow_run; the researchers found that SHA-pinning actions/checkout to releases before the July 20, 2026 backport, such as @08eba0b resolving to v4.3.0, bypasses the protections entirely.", "body_md": "For years, *pull_request_target* has been the canonical Pwn request vector: a workflow trigger that runs with repository secrets, yet can be triggered by anyone who opens a pull request from a fork. In 2026, GitHub responded with platform changes that make the default-case exploit significantly harder..\n\nThese changes represent real progress. Checkout protections, read-only cache tokens, and event policies close the paths that made exploitation trivial. **But they don't eliminate the vulnerability class. They shift it.** Attackers who previously exploited any *pull_request_target* workflow now need to find explicit opt-ins, outdated checkout versions, unprotected triggers, or script injection. \n\nIn [Part 1](https://www.wiz.io/blog/github-actions-security-threat-model-and-defenses) of our GitHub Actions research, we laid out the threat model and core defenses, and [Part 2](https://www.wiz.io/blog/github-actions-security-ai-powered-actions-vulnerabilities) covered AI-powered actions. This post updates that threat model for 2026: what GitHub's changes actually protect against, where the gaps remain, and how defenders need to adjust. The audit checklist at the end covers what to look for in your own workflows.\n\nRecognizing the endemic risk posed by Pwn requests, GitHub spent 2026 systematically closing the gap through a phased rollout of platform-level protections and action updates.\n\n| Date | Milestone & Action Taken | \n|---|---|\n| June 18, 2026 | Release of [actions/checkout@v7.0.0](mailto:actions/checkout@v7.0.0) , blocking fork code checkouts by default in untrusted triggers (pull_request_target, and workflow_run triggered by pull requests). | \n| June 26, 2026 | GitHub introduces read-only cache tokens at the platform level for untrusted triggers, preventing cache poisoning attacks. | \n| July 20, 2026 | Backported fork checkout protections across all active major versions of actions/checkout (v2.8.0, v3.7.0, v4.4.0, v5.1.0, and v6.1.0; v1 excluded). Workflows referencing mutable version tags (e.g., @v4) inherit these defenses automatically. | \n| September 10, 2026 | Fine-grained cache controls via cache-mode reached General Availability (GA). | \n| September 17, 2026 | Workflow Execution Protections reached GA, enabling fine-grained event policies and actor rules. | \n| December 2, 2026 | [Scheduled] GitHub begins enforcing the default event policy that blocks pull_request_target in public repositories without an explicit event policy. The rule has been in evaluate mode since September 17. Private and internal repositories are not affected. | \n\nWhile this sequence of platform updates significantly strengthens default security configurations for GitHub Actions, they don't remove all risk. \n\nThe December 2 enforcement is a default, not a ban. Maintainers whose automation depends on *pull_request_target* (labelers, preview deploys, fork CI with secrets) will add an event policy that allows it. Those are exactly the workflows that handle untrusted input with elevated privileges. The default policy also doesn't apply to private and internal repositories or to *workflow_run*. The rest of this section covers how these surviving workflows can still be exploited.\n\nOne of the most disconcerting findings in our research is how standard [supply chain hardening advice](https://www.wiz.io/blog/github-actions-security-guide) - specifically pinning actions to exact commit SHAs - can inadvertently bypass GitHub's platform defenses.\n\nBecause GitHub's backported protections rely on logic embedded within `actions/checkout` itself, pinning to SHA values prior to the July 20, 2026 backport release means the workflow runs legacy code without any safety checks. Our empirical testing across workflow triggers confirmed this discrepancy: referencing mutable tags like @v4 (resolving to v4.4.x) or pinned versions such as @v4.4.0 remain protected, whereas older versions like @v4.3.0 or SHA-pinned references to legacy releases (such as @08eba0b... resolving to v4.3.0) offer no protection.\n\nTo maintain protection, security teams using SHA pinning must ensure their refs are updated to match or exceed the minimum patched release versions: v2.8.0, v3.7.0, v4.4.0, v5.1.0, v6.1.0, or v7.0.0+.\n\nA second gap concerns workflows that bypass `actions/checkout` altogether. GitHub's protections are implemented within the action's code - any alternative method of fetching code operates unrestricted. Direct CLI invocations sidestep protections entirely:\n\nThird-party checkout actions implement their own git logic, so they don't inherit these protections. Any of them configured with a PR head ref will check out fork code unrestricted.\n\nGitHub's checkout protections only cover *pull_request_target* and *workflow_run*:\n\nPwn requests triggered in other event types besides pull_request_target (such as issue_comment) will not be blocked by this change. Further hardening of additional events may be explored in future releases.\n\nGitHub changelog\n\nThis creates an inconsistent security boundary: GitHub's cache controls [explicitly treat](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching) *issue_comment* as a low-trust trigger, while checkout protections do not. Workflows using *issue_comment* for bot commands (`/deploy`, `/test`, `/cherry-pick`) remain fully exploitable with checkout v7 - no `allow-unsafe-pr-checkout` flag required:\n\nOther privileged triggers have the same gap. *repository_dispatch* also runs with secrets, and it's exploitable when a workflow uses attacker-influenced payload data to choose a checkout ref.\n\nUntil GitHub extends checkout protections, these workflows have to enforce their own authorization, and it needs to hold from the moment the event fires until the code runs:\n\n- **Verify who triggered it.** For comment commands, check the commenter's actual repository permission (*getCollaboratorPermissionLevel*, write or above). Don't rely on *author_association*, which doesn't reflect current permissions. For *repository_dispatch*, limit who holds a token that can send the event, and don't treat the payload as proof of who sent it.\n\n- **Pin what was approved.** `refs/pull/N/merge` is mutable, so an attacker can push a malicious commit after a maintainer comments /deploy. Check out the exact head SHA, and abort if the PR was updated after the comment was posted.\n\n- **Treat the event payload as untrusted input.** Validate comment arguments and dispatch payload fields against a strict, anchored allowlist regex, and pass them to the shell through environment variables, never through `${{ }}` interpolation.\n\nA common misconception is that *pull_request_target* vulnerabilities require checking out fork code. In reality, attackers can achieve code execution without any checkout step, through script injection.\n\nWhen workflow authors interpolate event context directly into shell commands using `${{ }}` expressions, GitHub substitutes the value into the script before the shell runs, so attacker-controlled data becomes part of the command itself:\n\nAn attacker simply crafts a pull request with a malicious title: \n\n`\"; touch /tmp/pwned; echo INJECTED #`\n\nThe quote closes the string, the semicolon ends the echo, and everything after it runs as a new command with the job's secrets and token. None of GitHub's 2026 changes touch this: there is no checkout to block, and the vulnerability lies in how the workflow handles untrusted input.\n\nThe fix is to never interpolate untrusted input into a script. Pass it through an environment variable instead, so the shell treats it as data:\n\nThe double quotes around $PR_TITLE matter. Without them, the shell still word-splits and glob-expands the value.\n\nGitHub's read-only cache tokens protect the Actions cache service, but workflows can still exchange data through channels that remain unaffected.\n\n#### Artifact Poisoning\n\nThe `upload-artifact` and `download-artifact` actions operate independently of cache tokens. The classic attack chain uses an unprivileged *pull_request* workflow to upload attacker-controlled build outputs, then a privileged *workflow_run* to download and execute them:\n\nThis is exactly the workflow_run scenario checkout v7 was built to block, and it doesn't help here. The privileged workflow never checks out fork code; it downloads it. Unlike cache entries, there's no platform equivalent that marks artifacts from untrusted runs as tainted, so any workflow that downloads and executes artifacts from a less-trusted run inherits the risk.\n\n#### Self-Hosted Runner State\n\nOn non-ephemeral self-hosted runners, filesystem state persists between jobs. Local package manager caches (~/.npm, ~/.cache/pip), build directories, and installed tools live on the runner's disk, outside the Actions cache service, so read-only cache tokens don't cover them. Attacker code running under an untrusted trigger can write to these locations and poison the next trusted workflow scheduled on the same machine, including release and publish jobs. Use ephemeral runners, which are wiped after each job, for any workflow that untrusted events can trigger, and keep untrusted and trusted workflows in separate runner groups.\n\nMany security teams believe they have another safeguard in place: the \"Require approval for all external contributors\" setting under Settings -> Actions -> General. If enabled, fork PRs need maintainer approval before workflows run. Problem solved, right?\n\nNot quite. That setting only gates *pull_request* workflows, which run the fork's own workflow YAML. For pull_request_target, GitHub considers the workflow definition trusted (it comes from the base repository), so the approval setting doesn't apply and the run starts immediately.\n\nThe irony cuts deep: *pull_request_target* is the more dangerous trigger (secrets access, write tokens), yet it's the one that skips the approval requirement. If a *pull_request_target* workflow needs a human gate, put the privileged job behind an environment with required reviewers, or restrict who can trigger it with [Workflow Execution Protections](https://docs.github.com/en/actions/how-tos/administer/control-workflow-execution) actor rules.\n\nFinally, developers who need to interact with fork code can explicitly disable checkout protections by enabling the `allow-unsafe-pr-checkout` flag:\n\nWhile the parameter name is deliberately alarming, determined development teams or third-party workflows may still employ it to maintain legacy automation, effectively reopening the exact same execution vector as before. Security teams should pay special attention to any use of this flag.  \n\nTo sum up, GitHub's 2026 security rollout deserves credit. Default event policies, checkout protections, and read-only cache tokens make the trivial Pwn request (fork, PR, profit) significantly harder. Starting December 2, *pull_request_target* will no longer run in public repositories unless an event policy explicitly allows it. Cache poisoning through the Actions cache is mitigated. These are real wins.\n\nBut the vulnerability class isn't eliminated. It shifted. What remains exploitable: explicit opt-ins via `allow-unsafe-pr-checkout`, SHA-pinned versions predating the July 2026 backport, custom checkout scripts that bypass action-level controls, script injection requiring no checkout at all, unprotected triggers like *issue_comment* and *repository_dispatch*, artifact poisoning chains from *pull_request* to *workflow_run*, persistent state on non-ephemeral self-hosted runners, and private or internal repositories, which the default policy doesn't cover. **The attack surface shrinks, but what remains is harder to spot.**\n\nStart by auditing what you already run, then get ahead of the December 2 enforcement.\n\n[Wiz Code](https://www.wiz.io/platform/wiz-code) automatically scans GitHub Actions workflows for Pwn requests, script injection, version gaps, and risky configurations to cover this residual attack surface. Alternatively, use open-source tools like [zizmor](https://github.com/zizmorcore/zizmor/) or even these basic grep searches to start the audit:\n\nIn addition, run this script to resolve each SHA to its release tag, then compare against the patched versions:\n\n*pull_request_target* Enforcement\nStarting December 2, the default event policy will block *pull_request_target* in public repositories that have no explicit event policy. *workflow_run* is not affected, and neither are private or internal repositories. Before that date:\n\n- Check policy insights to see which *pull_request_target* runs would be blocked.\n- Decide per workflow whether it actually needs *pull_request_target.* If not, move it to*pull_request* . If a privileged follow-up step is still needed, a*workflow_run* workflow can handle it, but it must treat the PR run's artifacts as untrusted data, never execute them.\n- For workflows that legitimately need fork access, create an explicit event policy and use workflow file targeting to allow *pull_request_target* only for those files. Treat every file on that allowlist as high-risk: run it through the audit above before adding it, and re-audit whenever it changes.\n\n[State of SDLC Security 2026](https://www.wiz.io/reports/sdlc-security-report-2026)", "url": "https://wpnews.pro/news/the-pwn-request-reckoning-what-still-works-against-github-actions", "canonical_source": "https://www.wiz.io/blog/github-actions-security-pwn-request-reckoning", "published_at": "2026-10-26 13:24:12+00:00", "updated_at": "2026-10-26 14:15:48.945819+00:00", "lang": "en", "topics": ["ai-safety", "developer-tools"], "entities": ["GitHub", "GitHub Actions", "actions/checkout", "pull_request_target", "workflow_run"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/the-pwn-request-reckoning-what-still-works-against-github-actions", "markdown": "https://wpnews.pro/news/the-pwn-request-reckoning-what-still-works-against-github-actions.md", "text": "https://wpnews.pro/news/the-pwn-request-reckoning-what-still-works-against-github-actions.txt", "jsonld": "https://wpnews.pro/news/the-pwn-request-reckoning-what-still-works-against-github-actions.jsonld"}}