A pull request from an outside fork runs CI and writes an entry into a shared dependency cache that later builds on the main branch restore. Why is that a supply-chain risk, and what contains it?
answer
- restoring a cache runs someone's code
- who may write, may execute
- untrusted refs read down, never write up
- give the fork job nothing worth stealing
- rotate the namespace, do not hunt entries
basics
~20 sRestoring a cache places files chosen by whoever wrote it onto disk, and a build then executes them with the trusted job's credentials. Containment is scoping: untrusted refs get their own cache namespace, read from trusted entries but never write them.
solid answer
~50 sA cache restore is code execution with extra steps. The entry holds dependency binaries, compiled extensions, package-manager scripts — things the next build runs. If an untrusted contributor's job can write an entry that a privileged build restores, they have effectively landed a payload on the trusted runner, and it executes with that job's token, its cloud credentials and its publish rights. Nothing about it appears in a diff or a review, which is what makes it a genuine supply-chain vector rather than a theoretical one. The containment is layered. First, scoping: cache writes are namespaced per ref, so untrusted refs may read the default branch's entries but never write into that namespace. Second, do not run untrusted code in a job that holds anything worth stealing — no secrets, no privileged token, ideally an ephemeral runner. Third, treat restored content as untrusted input: run the installer so it verifies checksums against the lockfile instead of trusting whatever was on disk.
go deeper
Know that a cache can contain executable files and that whoever writes an entry influences what a later build runs, so caches from untrusted contributions are not automatically safe to reuse.
Explain the mechanism concretely: cache namespaces, which ref may write which namespace, and why restoring a resolved dependency tree is riskier than restoring a store the installer re-verifies against the lockfile.
Demonstrate the containment reasoning — untrusted jobs hold no credentials and run on ephemeral runners, privileged pipelines restore only from namespaces they trust, and rotation of the key namespace is your incident lever.
Own the org-wide posture: a standard split between trusted and untrusted pipelines, agreement that no job both executes outside contributions and holds deployment identity, and an honest account of how weak detection is here.
## Why a cache is an execution surface The usual mental model of a cache is inert data — a directory of downloaded tarballs. That model is wrong in the way that matters. Cache entries routinely contain executables and things that behave like executables: compiled native modules, package-manager lifecycle scripts, generated binaries in a `.bin` directory, compiler plugins, downloaded toolchains. A build that restores such an entry does not merely read it — it runs it. So the question "who can write this cache entry?" is the same question as "who can run code inside the jobs that restore it?" Once you hold those as the same question, the fork-PR case stops being subtle. Anyone in the world can open a pull request. Their branch's CI job runs their code by design — that is the point of testing a contribution. If that job's writes land in a namespace a default-branch build later reads, an outsider has obtained code execution inside a privileged pipeline. ## What the attacker gains The trusted job is where the value is: it holds the deploy credential or the OIDC-minted cloud token, the registry publish rights, the signing identity. A payload that runs there can exfiltrate those, or — more quietly — modify the artifact being built so that the shipped binary differs from the reviewed source. There is nothing in the repository to review, because the malicious bytes never entered the repository. This is why the class is treated as a supply-chain attack rather than a CI annoyance: the compromise is of the build, not of the source. A second, subtler variant does not need a fork at all. On a shared or long-lived self-hosted runner, one job's leftover state on local disk is another job's environment. A cache that is really "whatever the previous build left in this directory" has the same property, without any of the platform's scoping to protect it. ## The containment layers **Scope writes by ref.** The primary control is that a cache namespace has one writer class. A common and sound model is: a branch reads its own entries and may fall back to reading the default branch's entries, but writes only into its own namespace, and untrusted refs never write into the trusted one. Where the platform gives you that model, confirm it applies to fork pull requests specifically — the whole risk lives in the exact place where an untrusted ref is being built. **Do not give the untrusted job anything.** The strongest control is not cache-specific. A job that builds an outside contribution should have no secrets in its environment, a read-only token, no access to production, and ideally an ephemeral runner that is destroyed afterwards. Then even a successful poisoning yields an attacker a compromised throwaway machine with nothing on it. Deployment work happens on a separate, trusted trigger after review — never in the same job that first executed the contributor's code. **Treat restored content as untrusted input.** Restoring a dependency store and then running the installer is meaningfully safer than restoring a fully resolved dependency tree and skipping the installer, because the installer re-checks integrity hashes recorded in the lockfile and refetches what does not match. That is not a complete defence — a lifecycle script is still a script — but it converts "trust whatever is on disk" into "trust the lockfile", which is a reviewable artifact. **Keep the blast radius small.** Separate namespaces for privileged and unprivileged pipelines so the deploy job never restores an entry any pull-request job could have influenced. Short retention limits how long a bad entry can wait. And keep the manual version segment in your key so you can rotate the entire namespace instantly during an incident, rather than trying to find and delete individual entries. ## What to say about detection Be honest that detection is weak. The signals available are indirect: an unexpected cache size jump, entries written by a ref that should not write, integrity mismatches surfaced by the installer, egress from a build job to an address nothing should contact. This is why the emphasis sits so heavily on prevention and blast-radius reduction. The realistic response posture is: assume you cannot detect the entry, make sure the job that would restore it has nothing worth stealing, and make rotation cheap. ## The framing an interviewer is listening for The answer that lands is the one that names the equivalence up front — restoring a cache is running someone's code — and then reasons from it. Candidates who treat the cache as a performance feature with a security footnote usually miss that the containment is mostly about *who runs untrusted code with what credentials*, and that cache scoping is one instance of a general rule rather than a special case.
- How would a poisoned cache entry differ from a malicious dependency in the lockfile?The lockfile change is in the repository: it appears in the diff, gets reviewed, and is pinned by an integrity hash. A poisoned cache entry never enters the repository at all, so there is nothing to review and no history to audit. That invisibility is exactly why the cache path is attractive and why prevention has to be structural.
- Does keeping fork pull-request jobs free of secrets solve the problem on its own?It removes most of the prize but not the mechanism. The untrusted job can still write into a cache namespace, and if a later privileged job restores from it the payload executes where the credentials are. You need both: no secrets in the untrusted job, and no write path from that job into the trusted job's cache namespace.
- You suspect a cache namespace has been poisoned. What is your first move?Rotate the key — bump the version segment so nothing restores the existing entries — and stop any privileged pipeline that restores from that namespace until it is clean. Then rotate the credentials that were live in jobs which may have restored a bad entry. Hunting individual entries first wastes the window in which the token is still valid.
saying these in an interview costs you the question
- Calls a cache inert data that cannot execute anything
- Assumes fork pull requests cannot influence CI state at all
- Relies on scanning cache contents instead of scoping writes
- Runs deploy steps in the same job that built an outside contribution
- Thinks masking secrets in logs prevents cache-based exfiltration