Why do CI platforms withhold repository secrets from pipelines triggered by pull requests from forks, and what legitimate checks does that break?
answer
- unreviewed code runs in your pipeline
- anyone can open a pull request
- exfiltration needs no exploit
- split untrusted build from privileged action
- let the trust condition enforce it
basics
~20 sA fork pull request runs code, and often a pipeline definition, written by someone with no write access. If secrets were injected, a one-line change would print or exfiltrate them. Withholding them breaks checks that need real credentials, which must be restructured rather than re-enabled.
solid answer
~50 sThe pipeline that runs for a pull request executes the contributor's code — build scripts, tests, dependency install hooks — and on most platforms the pipeline definition from their branch too. Anyone on the internet can open that pull request. If the platform injected your deployment credentials, an attacker's first contribution would simply be a step that posts them to a server they control, with no review required. So platforms withhold secrets for fork-triggered runs and usually hand the job a read-only token, sometimes requiring a maintainer to approve the run at all. What breaks is anything needing a real credential: integration tests against a live third-party API, pulls from a private registry, review-app deployments, coverage or report uploads. The fix is to split trust rather than restore secrets — run the untrusted build and unit tests with no credentials, and do anything privileged in a separate run, on trusted code, against the artifact the untrusted job produced.
go deeper
Know that a pull request from a fork runs code nobody has reviewed, so the platform deliberately does not give that run access to repository secrets.
Explain that the pipeline definition and dependency install hooks from the contributor's branch also execute, so exfiltration needs no exploit, and name what the restricted token withholds.
Design the split: an untrusted build with no credentials producing an artifact, and a separate trusted run doing anything privileged over that artifact, with approval gates for genuine exceptions.
Set the org-wide rule that untrusted code and deployment credentials never share an execution context, and enforce it at the identity provider through subject conditions rather than through CI settings teams can change.
## The threat: poisoned pipeline execution On a public repository, opening a pull request is unauthenticated in the sense that matters: anyone may do it, and no maintainer has looked at the contents before CI runs. That pull request contains three things a CI system will happily execute — application code, test and build scripts, and (for fork-triggered runs on most platforms) the pipeline definition from the contributor's branch. If the platform injected repository secrets into that run, exploitation is trivial. There is no memory-corruption bug to find and no sandbox to escape: you add a step that reads the environment and sends it somewhere. Even without touching the pipeline file, a modified test, a build plugin, or a `postinstall` hook in an added dependency runs with the same environment. This is the poisoned-pipeline-execution class, and it is one of the most reliably exploited weaknesses in open-source infrastructure. ## What platforms do about it The standard posture across CI platforms is: - **No repository secrets** for runs triggered by pull requests from forks. - **A restricted job token**, typically read-only, so the run cannot write back to the repository or publish packages. - **Approval before the run happens**, often required for first-time contributors, so a maintainer at least glances at the diff before any code executes. - **Ephemeral hosted runners** by default, so a compromised job does not inherit state from previous builds. (Self-hosted runners on public repositories are a separate and worse problem, because the job runs on a machine you own with whatever it left behind.) There is also usually a trigger variant that runs the pipeline definition from the *base* branch with secrets available. It exists for legitimate needs — labelling, commenting on the pull request — and it is a well-known foot-gun, because a pipeline written that way can easily be made to check out and execute the fork's code, reintroducing the whole problem with privileges attached. Treat any such trigger as privileged: it must never run contributor code, and every step in it should be reviewed with that in mind. ## What legitimately breaks Withholding secrets is not free. The checks that stop working are real ones: - **Integration tests** that need a key for a live third-party sandbox. - **Private dependencies**: pulling base images or packages from a registry that requires authentication. - **Review-app deployments** that stand up a per-pull-request environment. - **Third-party reporting**: coverage, static-analysis, or bundle-size services that require an upload token. - **Signed or published preview artifacts.** Maintainers under pressure to make the badge green are exactly how secrets end up back in fork runs. ## Restructuring rather than re-enabling The pattern that works is to separate untrusted execution from privileged action: 1. **Untrusted stage.** Build, lint and unit-test the contributor's code with no credentials at all, and no write token. This produces a pass/fail signal and, if useful, an artifact. 2. **Trusted stage.** Anything requiring a credential runs in a separate context — after merge, or from the base repository — over trusted code, consuming the untrusted stage's artifact as *data* rather than executing it. 3. **Mint, do not store.** Where the privileged step needs cloud access, use OIDC federation with a subject condition that a fork-triggered run can never satisfy. That turns the boundary into an authorization rule enforced by the provider rather than a platform setting someone can toggle. 4. **Substitute rather than authenticate.** Many integration tests can run against a local fake, a recorded fixture, or a mirror that needs no credential. Reserve the live-credential suite for post-merge. 5. **Gate on approval.** For contributions where a real run is genuinely needed, require a maintainer to review the diff and trigger the privileged run explicitly. ## Private repositories are not exempt The same reasoning applies internally with a lower probability and the same impact. A contractor, an intern, or a compromised developer account can open a branch or an internal pull request. If every pipeline run in the repository is trusted with production credentials, then write access to the repository is production access. The fork case is just the version where the attacker needs no access at all. ## What to say in an interview Name the mechanism — unreviewed code executing in a context that holds credentials — rather than the platform toggle. Then propose the split into untrusted and trusted stages, and mention that a federation subject condition enforces the boundary at the cloud rather than inside CI configuration.
- A maintainer wants coverage uploads working on fork pull requests. How do you do it without exposing the token?Have the untrusted run produce the coverage file as an artifact with no credentials. Then a separate trusted run, executing the base branch's pipeline over that artifact as data, performs the upload. The contributor's code never runs in the context holding the token, and the report still appears on the pull request.
- How does OIDC federation help enforce this boundary?Because the identity token's subject describes the run's context, a trust condition pinned to a protected branch or environment simply will not match a fork-triggered run. The provider refuses the exchange. That is stronger than relying on a CI setting, since it survives pipeline edits and platform misconfiguration.
- Why are self-hosted runners especially dangerous for public repository pull requests?The job runs on hardware you own, potentially inside your network, and if the runner is persistent it carries state between builds — cached credentials, checked-out repositories, files on disk. Untrusted contributor code then gets a foothold rather than a throwaway container. If you must do it, use ephemeral, network-isolated runners.
saying these in an interview costs you the question
- Says fork pull requests are safe because the code is only tested.
- Re-enables secrets on fork runs to make a check pass.
- Uses a base-branch-privileged trigger that checks out fork code.
- Thinks private repositories make the problem disappear.
- Relies on reviewing the diff after CI has already executed it.