Modern software is built by robots reacting to strangers. This supply-chain research found two public workflows where one pull request was enough to put production secrets and package publishing rights within reach.
The opening
A pipeline watches a public repository and, the moment anyone opens a pull request, springs into action: installing dependencies, building previews, running scripts. The entire security model rests on one assumption — that the robot will not run the stranger’s code with the keys to production.
Two workflows in a public repository broke that assumption with a single misconfigured trigger. The cost of the attack was one pull request, opened from a fork by an account with no relationship to the project at all.
Three mistakes
The vulnerability is not one flaw. It is three ordinary configuration choices that are individually common and collectively fatal.
pull_request_targetRuns in the context of the base repository, which means the job has access to repository secrets.
ref: head.shaThe checkout step pulls the attacker’s branch. Trusted context, untrusted code, same job.
authorize: trueA gate that looks protective and enforces nothing — the environment behind it had no protection rules.
Each of these appears in thousands of public repositories. Together they hand out the keys.
Put together, the pipeline checks out attacker-controlled code and then runs an install on it, executing the attacker’s postinstall lifecycle script with production secrets in the environment.
The escalation
Stealing whatever happens to be in the runner’s environment is bad. What made this critical is where the runner’s own credential leads.
The GITHUB_TOKEN that the checkout action helpfully writes into .git/config as an HTTP auth header carried both actions:write and contents:write. With the first it can dispatch any workflow. With the second it can push a branch containing a modified publish workflow and dispatch that one — reaching the publish-only secrets that the original job was never given.
Six hops, four of them automatic. Nothing between the fork and the runner required a human. Five links were proven live; the sixth was deliberately withheld.
No account relationship, no review, no interaction. The workflow triggers itself.
postinstall executes on the runnerOutbound callback confirmed from inside the job, with production secrets loaded.
GITHUB_TOKEN extracted from .git/config, left there by the default persist-credentials behaviour.
actions:write and contents:write both confirmed against the API.
HTTP 204. The publish job ran, built the packages and reached the publish step with the deploy token loaded.
Every prerequisite confirmed. This is where we stopped.
The test pull requests were opened and closed within seconds. The last rung is the one nobody should ever walk.
Environment variables came back real: organisation and project identifiers from the deployment platform, the runner’s own token, and confirmation that the publish workflow would accept a dispatch from it. The triggered publish run failed at the final step for one reason only — the dispatch payload intentionally omitted the version-bump input.
Root cause
The authorisation gate was theatre. It looked like a control, carried a reassuring name, and enforced nothing, because the environment it depended on had empty protection rules — no required reviewers, no wait timer. A gate like that is worse than no gate, because it manufactures confidence.
Underneath it sat the classic pattern: a trigger that runs with the base repository’s secrets, a checkout of untrusted code, credentials persisted to disk by default, and no permissions block narrowing the token. A timeline review showed the safe trigger had been changed to the dangerous variant at a specific commit. Someone made this repository exploitable in a single edit.
Business impact
Malicious versions of public packages that handle authentication and transactions — reaching every downstream consumer who installs an update.
A deploy token, likely a personal access token, surviving well past the original attack window.
Hosting-platform tokens allowing arbitrary code to be pushed to the production frontend.
Running untrusted code in a trusted context is the single most dangerous pattern in CI. Everything else in this chain follows from it.
An authorisation gate is only as real as its protection rules. Check the environment, not the job name.
The runner credential is a pivot, not a convenience. Disable credential persistence and set a least-privilege permissions block, and the escalation closes.
Validate every link; stop before the destructive one. The deliverable is a proven path, not a detonated one.