Caches reduce repeated dependency downloads, shorten feedback loops and control CI/CD infrastructure costs. Yet a cache is not merely harmless reusable data. It can contain packages, scripts, compiled objects and executables that a later workflow will trust and run.

That makes caching a security boundary. If a low-trust pull request workflow can modify state later restored by a privileged release job, the cache becomes a bridge between two otherwise separate environments. DevOps teams therefore need to understand how poisoning crosses that bridge, where the dangerous restore paths exist and how to retain the speed benefits without inheriting the attacker’s code.

Recent Incidents Removed Any Remaining Doubt

The compromise in May 2026 of TanStack demonstrates the full attack chain in production. As per the official postmortem by the project, the attacker employed a dangerous pull_request_target workflow, cache poisoning at the fork-to-base boundary, and runtime extraction of the OIDC publishing token.

A poisoned 1.1 GB pnpm-store cache was stored with a key requested by the legitimate release workflow. During the restoration of the entry, code controlled by the attacker executed within the scope of a job with publishing privileges. Within six minutes, 84 malicious versions in 42 @tanstack/* packages were uploaded on npm. The publish workflow wasn’t tampered with, and no npm token was stolen. The malicious versions were deprecated and removed.

GitHub added read-only access for caches to some untrusted triggers. This is an important change, but there is an even more important lesson here: even the protection of source and secret storage can’t help if you have a contaminated state running inside the job.

How a Poisoned Cache Reaches the Release Pipeline

It starts with the ability for an attacker to manipulate the input to a pipeline by means of a pull request, an issue, or another similar trigger. A job with vulnerabilities will run this input code and gain the right to update a shared cache. The attacker adds a tampered dependency, script, or a part of the build into a location that the trusted pipeline expects to restore.

The attack flow is concise:

Untrusted input → vulnerable workflow → writable cache → trusted restore → privileged execution → compromised release

Timing separates the attack steps and makes them hard to notice. The first job may complete without any secrets getting accessed or being published at all. After a few hours, a scheduled build, push, or release job will restore the entry and execute the code with elevated permissions, cloud credentials, or an OIDC identity. The poisoned package will be delivered by the normal process to the organization’s registry.

This is different from credential theft or a version tag movement. Cache poisoning exploits reusable pipeline context. It works since the consumer trusts a cache hit to mean that it’s fresh and safe, but neither.

Map Who Can Write and Who Can Restore

Begin your assessment with a writer-reader matrix. For each cache, determine what workflows can produce or modify it, what triggers them, and whether an external entity can affect their run. In the reader column, specify the branches, tags and jobs that can restore the entry, as well as credentials that will be available after restoring.

Be especially careful about generic restore-keys. If there’s a precise-key miss, it may restore to an older entry or a namespace used by other trust levels. Examine the persistent workspaces on self-hosted runners, too.

Your inventory should not be limited to the package-manager metadata. The outputs from compilers, the layers in container images, downloaded toolchains and the incremental build trees contain executable content. According to GitHub cache security recommendations, all restored files should be considered untrusted inputs.

The rule is clear and straightforward: if the state is writable in a low-trust context and accessible in a privileged workflow, it is a security flaw.

Break the Attack Chain Without Disabling Caching

Disabling the cache almost never makes sense. For npm ci, with a 3-run test involving 80 packages weighing 28.6 MB, the median dropped from 5.091 seconds with no cache present to 0.759 seconds with a full cache, a 6.7 times improvement. While this network-bound test is merely instructive, and certainly not production-ready, it helps explain why disabling caches is undesirable.

Instead, isolate trust levels. Jobs that do not trust should restore rather than save; while release jobs need their own namespace, narrow fallbacks, and regenerate security-sensitive artifacts. The following is merely a shortened example:

# Untrusted pull request: restore only

permissions:

contents: read

steps:

– uses: actions/cache/restore@v5

with:

path: ~/.npm

key: pr-${{ github.base_ref }}-${{ hashFiles(‘package-lock.json’) }}

# Trusted push workflow: separate namespace

– uses: actions/cache@v5

with:

path: ~/.npm

key: release-${{ github.ref_name }}-${{ hashFiles(‘package-lock.json’) }}

Namespacing is not authorization. Limit access to a read-only level on the platform or storage level. Tie actions to specific commit hashes, use least privilege, and limit OIDC claims by repository, branch and workflow. Use transient runners and validate input data before executing them.

The GitHub June 2026 control addresses one way that workflows can be triggered externally. However, teams should investigate cache storage, other CI platforms, runner configurations and compromised collaborators.

Detect the Poisoned State Before a Release Uses It

Detection must cover cache activity, not just commits. Alert on cache writes from unexpected events, unusual changes in cache size, new executable files and privileged jobs restoring entries created by unfamiliar runs. At runtime, monitor unexpected child processes, outbound connections and access to cloud metadata services, credential files or token endpoints.

Security teams may already collect antivirus and malware telemetry from employee devices, but ephemeral and self-hosted runners need equivalent process, network and file-integrity visibility. A successful build is not necessarily a clean build; an attacker can preserve expected output while quietly harvesting credentials or altering a package.

Record the workflow, event, commit, actor and trust level that created each entry, then retain that provenance with the build logs. A release job should know both which key it restored and who could populate it.

Respond as Though the Release Pipeline Was Compromised

Removing the suspicious entry is just the beginning of containing the breach. Stop any other releases, disable the workflows affected, retain all logs and runner telemetry. Clear the related caches, invalidate the active OIDC sessions and rotate all credentials that the compromised job had access to. Ensure this rotation is done in a clean environment.

Create the impacted artifacts anew on a new set of runners, then compare those artifacts, binary packages, and attestations against good release versions. Analyze logs from registries, repositories, clouds and networks from the time the cache was created, not only since the bad release was made. If bad artifacts were distributed to downstream customers, notify them immediately of the impacted version numbers and window of vulnerability.

The self-hosted runner that ran the attack payload must be either re-built or retired. Simply deleting one file does not guarantee no persistence.

Treat Cache Integrity as Build Integrity

The problem is not caching. The problem is enabling reuse of state that crosses trust boundaries without isolation, provenance, or validation. Any DevOps team should discover the caches that are restored as part of their release process, determine which jobs can write to them, and eliminate untrusted execution access to privileged publish operations.

If a pipeline cannot prove where it got its inputs from, then it cannot generate trustworthy software products. Cache validation must therefore be treated with the same level of diligence as source validation, secret handling, and release approval processes, while retaining the performance advantage that made cache use desirable.

Frequently Asked Questions

1. How does CI/CD cache poisoning work?
Attackers manipulate cached dependencies or build artifacts that trusted workflows later restore and execute, potentially compromising software releases.
2. Can GitHub Actions caches expose release credentials?
Yes. If malicious cached content executes inside a privileged workflow, it may access publishing capabilities, OIDC tokens or other available credentials.
3. How can DevOps teams prevent cache poisoning?
Separate trusted and untrusted caches, restrict write permissions, use isolated runners and validate restored artifacts before privileged execution.