- Published on
Pull Requests Go Both Ways: Exploiting a Flaw in GitHub Actions Deployment Protection Rules
At QuantCo, we follow a GitOps approach: privileged workflows — from production deployments to interactions with GitHub, Okta, and other services — are driven by GitHub Actions. Access to their credentials is controlled through GitHub Actions environments. At the same time, every colleague can propose changes to many repositories with what GitHub calls write role. To prevent unauthorized changes, we protect the main branch with branch rulesets that require review approvals, and we restrict our environments to main using the deployment branches feature. This way, only reviewed and approved code on main can access privileged credentials — or so we thought.
In February 2025, we discovered that any collaborator with push access to any branch could bypass the deployment branch restriction and access protected environment secrets and OIDC tokens by leveraging the pull_request_target workflow trigger. Importantly, we were affected by this vulnerability even though none of our workflows used the pull_request_target trigger — the attacker could introduce it themselves. We reported the issue to GitHub through their Bug Bounty program on HackerOne and received a bounty for the finding after it has been fixed in December 2025.
In this post, we describe the vulnerability, explain how it could be exploited, and discuss its implications.
Background: Environments and Deployment Branches
GitHub Actions environments allow repository administrators to configure protection rules that gate access to secrets and specific OIDC token claims (OIDC is a secure way of authenticating access to external resources (e.g. cloud storage) from GitHub Actions workflows without storing credentials). Despite the name, environments are not limited to deployments — they can protect any privileged workflow, such as one that manages GitHub organization settings, rotates credentials in our Single-Sign-On (SSO) system, or interacts with cloud infrastructure. One key protection mechanism is the deployment branches feature, which restricts which branches can use an environment and thereby controls access to its secrets and OIDC token claims.
A typical security setup looks like this:
- The
mainbranch is protected by a ruleset requiring pull request reviews before merging. - All collaborators have push access to the repository, but cannot push to
maindirectly. - An environment
productionis configured withmainas the only deployment branch. It holds secrets or is used to issue OIDC tokens for accessing external services.
Under this model, only workflows running on main should be able to access the production environment. This guarantees that any code interacting with privileged resources has passed the review process.
At QuantCo, we rely heavily on this pattern. Our workflows use OIDC tokens scoped to protected environments to authenticate with external services such as cloud providers, GitHub itself, and the SSO system. The deployment branches restriction is the mechanism that ensures only the main branch — and therefore only reviewed code — can obtain these tokens.
The Vulnerability
The core issue was that GitHub Actions allowed workflows on arbitrary branches to access environments restricted to specific deployment branches. The attack vector was the pull_request_target event trigger, which has a subtle property: unlike the regular pull_request trigger that runs the workflow from the PR's head branch, pull_request_target runs the workflow as defined on the base branch of the pull request.
When a pull_request_target workflow requested access to a protected environment, GitHub checked the deployment branch restriction against the head branch of the PR — not the base branch where the workflow actually runs. This mismatch meant that an attacker could:
- Create a branch with a malicious
pull_request_targetworkflow that accesses a protected environment - Open a PR with their malicious branch as the base and the protected branch (e.g.,
main) as the head - The workflow from the attacker's branch executes with access to the protected environment, because the deployment branch check passes against
main
Crucially, the attacker introduces the pull_request_target workflow themselves. Your repository did not need to use pull_request_target in any of its existing workflows to be vulnerable. Any repository that relied on the combination of branch rulesets and deployment branch restrictions to protect an external resource — whether via OIDC tokens or environment secrets — was affected.
The Attack in Detail
Consider a repository where main is protected and an environment env restricts deployments to main. An attacker with push access to the repository (but not to main) can access the environment as follows.
Step 1: Clone the repository and create a new branch from an older commit on main:
git checkout HEAD~1
git switch -c attack
Branching from HEAD~1 rather than HEAD is necessary so that main has a commit that attack does not, which allows opening a PR from main into attack.
Step 2: Add a malicious workflow on the attack branch:
# .github/workflows/attack.yml
on:
pull_request_target:
branches: [attack]
jobs:
leak:
runs-on: ubuntu-latest
environment: env
steps:
- run: echo $((${{ secrets.SECRET }} + 1))
The branches: [attack] filter ensures this workflow only triggers for pull requests targeting the attack branch. The + 1 arithmetic bypasses GitHub Actions' automatic secret masking, which only redacts exact matches of secret values in logs.
Step 3: Push the branch and open a pull request with attack as the base branch and main as the head branch. The PR direction is reversed from what you might expect and is summarized in the table below:
| Branch | PR role | Controlled by attacker? | Workflow executed from? | Used for deployment branch check? |
|---|---|---|---|---|
attack | Base (target) | ✅ | ✅ | ❌ |
main | Head (source) | ❌ | ❌ | ✅ |
This is the core of the mismatch: GitHub ran the workflow from the base branch (attack), but evaluated the deployment branch restriction against the head branch (main).
Because pull_request_target executes the workflow from the base branch (attack), the attacker's malicious workflow runs. But GitHub evaluated the deployment branch restriction against the head branch (main), which satisfies the restriction. The workflow was granted access to the env environment, and the secret appeared in the logs:
> Run echo $((*** + 1))
1337
Impact
It's worth noting that exploiting this vulnerability required push access to the repository — meaning the attacker would need to be at least a somewhat trusted collaborator, not an external party.
That said, for organizations that rely on environment protection as an access control boundary, the implications were meaningful:
Bypassing deployment branch restrictions: The deployment branches feature's guarantee — that only specific branches can access an environment — could be circumvented by any collaborator with push access.
OIDC token exfiltration: GitHub's documentation explicitly recommends using environment protection rules to secure OIDC-based deployments. Since the malicious workflow ran within the protected environment, it received a valid OIDC token scoped to that environment. For organizations using OIDC to authenticate with cloud providers or other external services, this could enable unauthorized access to those systems.
Undermining GitOps access control: In a GitOps setup where everyone has write access and branch protection enforces review requirements, the deployment branches feature is the critical control that prevents unauthorized access to privileged resources. This vulnerability undermined that control, allowing any collaborator to bypass the review process and interact with production infrastructure, external services, or any other system gated by environment protection.
At QuantCo, where every technical colleague has write access to many repositories and branch rulesets are the primary mechanism for enforcing the review process, this was a relevant concern — though the risk was limited to trusted insiders rather than external attackers.
The Fix
GitHub announced the fix on November 7, 2025, with the changes taking effect on December 8, 2025. The fix addresses two aspects:
pull_request_targetexecution: Workflows triggered bypull_request_targetnow always execute using the default branch as their source, regardless of the pull request's base branch. This prevents an attacker from running a malicious workflow defined on an arbitrary branch.- Environment branch protection evaluation: Branch protection rules for environments now evaluate against the execution reference rather than the pull request head. For
pull_request_target, this means evaluation occurs against the default branch.
Together, these changes close the gap we reported: the attacker's malicious branch can no longer serve as the source of a pull_request_target workflow, and the deployment branch check no longer evaluates against the wrong branch.
We verified the fix by reproducing the original attack setup and confirming that the malicious workflow no longer executes.
Disclosure Timeline
| Date | Event |
|---|---|
| February 20, 2025 | We reported the vulnerability to GitHub via HackerOne. |
| February 20, 2025 | GitHub confirmed they could reproduce the issue. |
| March 11, 2025 | GitHub triaged the vulnerability and confirmed they were working on a fix. |
| November 7, 2025 | GitHub published a changelog entry announcing the upcoming fix. |
| December 8, 2025 | The fix took effect. |
| March 2, 2026 | GitHub requested a retest. |
| March 5, 2026 | We confirmed the fix resolves the vulnerability. |
| March 6, 2026 | GitHub awarded bounties and cleared the report for public disclosure. |
GitHub classified the vulnerability as Medium severity and awarded a bounty of $4,000. We are donating the bounty to GiveWell's All Grants Fund, and we appreciate that GitHub matches the donation through their program.
Takeaways
Security is a journey, not a destination. Bugs like this one — arising from subtle interactions between two individually well-designed features — are difficult to catch proactively. Even a thorough security review of our workflow configurations would likely not have uncovered this, since the issue was in how GitHub's platform evaluated branch protection rules rather than in anything we configured incorrectly.
We discovered this vulnerability precisely because we actively invest in our CI/CD automations and continuously think about their security properties. That kind of ongoing engagement is what turns potential blind spots into bug reports — and ultimately into fixes that benefit the broader community.