Yaamlabs
Case study 18

CI token allowed a fork to push production

SaaS

A pull request from a fork could run the pipeline. The pipeline held a token that could write to production.

Cloud & CI/CD review, SaaS platform, severity HIGH, reported 2025

At a glance

  • Challenge. A workflow triggered by pull requests from forks ran author-controlled code in a job that held a token with write access to production infrastructure.
  • Approach. We read every workflow for untrusted triggers, secret exposure and credential scope, then showed from a controlled fork that the token was readable and valid.
  • Result. Privileged work moved to a separate workflow that runs after review, the token was rotated and scoped down, and we retested from a fresh fork.
  • Impact. Anyone able to open a pull request from a fork, with no write access and no merge, could have taken a token that writes to production infrastructure.
  • Classification. CWE-829 Inclusion of Functionality from Untrusted Control Sphere, OWASP Top 10 (2021): A08 Software and Data Integrity Failures. Component: CI workflow triggered by fork pull requests.

The challenge

Fork-based contribution and privileged CI are each reasonable on their own. Open the repository to pull requests from forks and you get contributions. Give the pipeline a deployment credential and you get continuous delivery. Put them in the same workflow file and you have handed the deployment credential to anybody who can open a pull request.

The mechanism is the trigger. A workflow that runs on pull requests from forks runs code the fork author controls, and not only their application code. The build scripts, the test command, the dependency manifest and everything those invoke are all author-controlled. If the job that runs them also receives secrets from the repository environment, the secrets are sitting in a process the author owns.

An attacker needs no write access for this and does not need to be trusted. They need a fork and a pull request. They never have to be merged, because the pipeline runs before review, which is the entire point of running it.

It survives review because the two halves are owned by different people. The engineer who opened the repository to external contributions was not thinking about the deployment token. The engineer who put the deployment token in the environment was not thinking about who can trigger a build. Neither of them was wrong about their own change.

Our approach

We read the workflow definitions before touching anything. Configuration review comes first in a pipeline engagement for the same reason source review comes first in an application one: the workflow tells you which jobs run untrusted input, and there is no point probing the ones that do not.

We read every workflow for five things:

  • Which triggers run on input originating outside the repository
  • Which of those jobs receive secrets, directly or through an environment
  • Whether privileged and unprivileged work shared a job or a runner
  • The scope of each credential, meaning what it could write and where
  • Whether self-hosted runners persisted state between jobs

One combination lined up: an event that fires on pull requests from forks, and a job in the same workflow holding a credential with write access to production infrastructure.

We demonstrated it in a controlled fork against a non-production target and captured the credential exposure. Enough to show the token was present, readable and valid, and nothing past that. Pushing to production was never necessary to prove the path, and doing it would have made the evidence worse rather than better.

Reported the same hour, with the workflow file, the line, the trigger and the reproduction steps.

From finding to fix

  1. Reviewed the CI pipeline
  2. Found a build token scoped too broadly
  3. Read the production token from a test fork
  4. Scoped the token down, retested

The outcome

The trigger was changed so untrusted code no longer runs in a job holding deployment secrets. The privileged work moved into a separate workflow that runs after review, against a trusted ref.

The token was rotated, because it had been exposed to a process we controlled and there is no way to prove it was never exposed to anybody else. It was then scoped down, so the credential the pipeline holds now can do what the pipeline needs and nothing more.

We retested after the workflow change, from a fresh fork. Reported as HIGH in 2025. Retest included in the engagement.

Check your own system

  • Search your workflows for triggers that run on pull requests from forks, such as pull_request_target, and list which of those jobs can read secrets.
  • Keep fork code out of any job that holds deployment credentials, and run privileged steps in a separate workflow after review against a trusted ref.
  • Scope each pipeline credential to the repository and environment it deploys to, and rotate any token that untrusted code could have read.
  • Check whether self-hosted runners keep files or processes between jobs, and keep fork builds off runners that also run privileged jobs.

Read more case studies, see Cloud & Kubernetes Security, or tell us about your system on the contact page.