Fork-Triggered Build
also called Privileged Trigger, Pwn Request
A continuous-integration trigger that runs a workflow in the base repository's privileged context while its inputs come from an outside contributor - so the pipeline executes attacker-authored code with its own credentials.
A team does everything the secrets guidance asks. No credentials in the repository, the cloud role assumed through workload identity federation, the registry token in a managed store, and a build that never prints it. Over a weekend an attacker publishes a malicious version of the team's package, and the audit trail shows the push came from the team's own pipeline.
The privilege did not come from a stored secret. It came from the trigger. A workflow configured to run in the base repository's context, so it can post review comments or apply a label, holds the base repository's identity and secrets. If it then checks out the pull request head and runs the project's own scripts, contributor-written code executes with the pipeline's credentials attached. Security researchers named the pattern the pwn request, and it remains one of the most common ways a well-run pipeline is compromised.
Why it matters
Most secret-handling guidance answers where is the secret stored. This failure answers a different question: whose code can run while it is available. Federation, short-lived tokens and narrow scopes all behaved as designed, because the token really was issued to the team's own workflow on the right ref.
For a library with on the order of 30,000 weekly installs, a malicious version reaches resolvers within about 15 minutes of publish, and a weekend window of 60 hours is the whole incident. Nothing looks anomalous, because publishing from the pipeline is expected.
Implementation patterns
- Two triggers, two privilege levels. Fork pull requests run on a trigger with no secrets and a read-only token, and anything needing credentials runs as a separate job from a trusted ref.
- Never check out untrusted code into a credentialled job. If a privileged job must see the contribution, it reads it as data and does not execute its scripts, build files or hooks.
- Human approval per run, approving a specific commit rather than a contributor.
- Pin the checkout step and keep it current, since recent versions refuse by default to fetch fork code inside a base-context workflow.
- Alert on publishes with no matching approved release run, keyed on run identifier rather than actor.
Industry example
The documented class is specific to the base-repository trigger offered by hosted CI platforms,
pull_request_target being the widely studied example. Security research through 2023 and 2024 found it in
public repositories of large software vendors, where a privileged workflow checked out fork code to run
linting and handed over a write-scoped token.
The ecosystem's response is instructive because it changed a default rather than publishing more advice. From version 7, the standard checkout action blocks fork pull request checkouts inside base-context and workflow-run workflows unless an explicit unsafe flag is set. Guidance had been available for years and the class persisted; changing one default closed most of it.
Failure scenarios
- Test script modified in the fork, executed by the privileged job, exfiltrating the environment.
- Build configuration as the payload: a changed build file, lifecycle hook or makefile, none of which look like code in review.
- A build secret passed as a build argument, baked into an image layer and readable by anyone who can pull the image: the same root cause, a secret correctly stored and then handed somewhere that remembers it.
Trade-offs
Splitting triggers costs contributor experience and maintainer time: an outside contributor no longer gets full test results automatically, and approving runs adds about 20 minutes of latency per release.
The alternative is a pipeline whose credentials are available to anyone who can open a pull request. For a public repository that publishes artefacts other people install, the approval cost is small against distributing a backdoor to every consumer of the package.
When not to use it
On a private repository with no outside contributors there are no forks, so the class does not exist, and adding human approval to every credentialled job buys friction and nothing else.
The control that still earns its place everywhere is narrower: never hand a secret to a builder in a way that persists, and treat the build cache as readable. A leaked layer is a leak regardless of who opened the pull request.
Interview question
Q: "Our pipeline has no stored credentials at all: everything is federated and short-lived. An outside contributor still got it to publish a malicious release. Explain how, and tell me what you would change first."
What a strong answer covers: that privilege came from the trigger rather than a stored secret; why federation and token scoping behaved correctly and did not help; the pwn request mechanism; the rule that a job may hold credentials or run author-controlled code but never both; the safe-default checkout behaviour as the single change that closes most of the class; the publish-without-approved-run alert; and the limit, that a private repository with no fork traffic needs none of it.
Quick check
Quiz: Why does workload identity federation not prevent a pwn request? — It removes the stored key but does not narrow who can make the pipeline act. The minted token is correctly scoped and issued to a workflow that really is yours.
Flashcard: State the rule that removes the fork-trigger class. — A job may hold credentials, or run code the pull request author controls. Never both.