Safeguard
Supply Chain•

Can a fork pull request poison your GitHub Actions cache?

Cache poisoning plants files a later privileged job restores. How GitHub scopes caches for pull requests and what maintainers should do about it.

Safeguard Security Team
3 min read

Cache poisoning is a real class of attack, and GitHub documents it. Pull request runs are scoped so they cannot write caches the default branch restores, and low-trust triggers are restricted. The risk returns if you opt out of that default or restore untrusted caches into privileged jobs.

Definition

A dependency cache stores files, such as downloaded packages, so later runs start faster. Cache poisoning means an attacker writes malicious files into a cache that a later, more trusted workflow restores and runs. GitHub notes cache contents are not signed or verified, and any run that can read a cache may extract its contents.

What GitHub says about scope

  • Anyone with read access can open a pull request and read cache contents, and forks can create pull requests on the base branch and access caches on the base branch. So never put tokens or credentials in a cache path.
  • A pull request run can restore caches from the base branch, including base branches of forks.
  • The pull_request event is not affected by the new restriction. Caches it creates are scoped to the merge ref and cannot be written to the default branch's scope.
  • Triggers that can be started by people without write access, such as pull_request_target, issue_comment and workflow_run, are restricted from creating or overwriting caches in the default branch scope. A repository can opt out by declaring a write-capable cache mode.

How an attack would work

  1. A workflow on a low-trust trigger checks out contributor code and runs a build that saves a cache.
  2. The attacker's build writes a tampered package or tool into the cache path.
  3. A later release or deploy workflow restores that cache with the same key.
  4. The tampered file runs with the release job's secrets.

The default restrictions are designed to stop step 2 from reaching step 3. Opting out removes the guard.

Maintainer checklist

  1. Search workflows for any declared write-capable cache mode on a low-trust trigger. Remove it unless the job never touches untrusted input.
  2. Do not restore caches in release, publish or deploy jobs. Rebuild from locked dependencies instead.
  3. Use lockfiles and verify package integrity, so a poisoned cache cannot silently change versions.
  4. Keep cache keys specific, including a hash of the lockfile.
  5. Treat any cache produced by an overridden low-trust job as untrusted in every workflow that restores it.
  6. Never cache credentials or tokens.

Does it matter for small repositories?

If your release job restores a cache and holds publishing credentials, yes. The cost of rebuilding without a cache is minutes. The cost of a poisoned release is a supply chain incident.

Try it yourself

Click through it at your own pace, or open it full screen.

Look at the dependency inventory, which is the list a lockfile based rebuild should match.

Where Safeguard fits

Safeguard's SBOM and dependency scanning list what actually goes into a build, so unexpected version changes stand out. A free scan shows your own inventory.

Quick check

Can a normal pull_request run write to the default branch cache?

No. GitHub scopes pull_request caches to the merge ref.

Should release jobs restore caches?

It is safer not to. Rebuild from a lockfile so a cache cannot change what ships.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.