Automation through GitHub Actions makes continuous integration effortless, but misconfigured workflows can inadvertently hand full write access or secret keys to malicious actors.
One of the most dangerous patterns in GitHub CI/CD security is known as a "Pwn Request."
📌 Credit & Reference: This article summarizes key insights from the original research published by Jaroslav Lobačevski on the GitHub Security Lab Blog.
🔍 What is a "Pwn Request"?
A Pwn Request occurs when an attacker submits a Pull Request (PR) from a public fork that triggers an automated workflow running with elevated privileges (write permissions or access to repository secrets).
Because build and test automation inherently runs code defined within the repository, untrusted PRs can manipulate that execution context. An attacker can achieve code execution by:
- Modifying build scripts (e.g.,
Makefile, PowerShell, orpackage.jsonscripts). - Adding malicious package pre-install/post-install hooks (
npm install). - Writing arbitrary payload code within test suites executed during CI.
🚨 The Vulnerable Pattern: pull_request_target + Explicit Checkout
To protect repositories, GitHub's default pull_request trigger strips write permissions and secret access from PRs originating from external forks.
However, developers often need workflows to comment on PRs or label them. To allow this, GitHub introduced pull_request_target, which:
- Runs with write access to the target repository and access to secrets.
- Evaluates in the context of the target repository rather than the untrusted fork.
The Deadly Mistake
The vulnerability happens when developers explicitly check out the untrusted head commit of the incoming PR inside a pull_request_target workflow:
# ❌ INSECURE EXAMPLE
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
# Explicit checkout of untrusted PR code in a privileged context!
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install
- run: npm build
When npm install or npm build executes, it runs scripts provided by the untrusted PR while holding a write-capable GITHUB_TOKEN in memory!
🛡️ The Secure Architecture: pull_request + workflow_run
To safely process PRs from external forks when elevated actions (like commenting or labeling) are required, separate the unprivileged build step from the privileged reaction step using artifacts.
Step 1: Unprivileged Build (ReceivePR.yml)
Runs in an isolated environment without write permissions or secrets access.
name: Receive PR
on:
pull_request:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: /bin/bash ./build.sh
- name: Save PR number as artifact
run: |
mkdir -p ./pr
echo ${{ github.event.number }} > ./pr/NR
- uses: actions/upload-artifact@v4
with:
name: pr
path: pr/
Step 2: Privileged Reaction (CommentPR.yml)
Triggers only after the unprivileged workflow completes, running safely in the base repository context.
name: Comment on PR
on:
workflow_run:
workflows: ["Receive PR"]
types: [completed]
jobs:
comment:
runs-on: ubuntu-latest
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: pr
path: ${{ runner.temp }}/pr
run-id: ${{ github.event.workflow_run.id }}
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const issue_number = Number(fs.readFileSync('${{ runner.temp }}/pr/NR', 'utf8').trim());
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue_number,
body: 'All checks passed successfully! Thank you for your contribution.'
});
💡 Key Takeaways for Developers
- Treat external PRs as untrusted input: Never execute code or scripts from external contributors inside a privileged runner.
-
Avoid checking out untrusted refs under
pull_request_target: Use standardpull_requestwhenever code compilation or testing is required. -
Decouple tasks: Use the
workflow_runevent pattern to pass safe, passive data (like coverage text or PR numbers) from unprivileged workflows to privileged ones.
*For full technical details, refer to the original research by GitHub Security Lab: Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests*.

Top comments (0)