GitHub Actions

GitHub Actions Cache Security: Avoid Poisoned Builds

Dependency caches are useful because they are invisible when they work. That same invisibility makes them worth auditing before a release workflow trusts restored dependencies.

Key takeaways for GitHub Actions cache security

  • GitHub says actions/cache searches for an exact key, then partial key matches, then ordered restore-keys when they are provided. [1]
  • GitHub says a cache miss creates a new cache if the job completes successfully, and existing cache contents cannot be changed under the same key. [1]
  • GitHub warns not to store access tokens or login credentials in cached paths because pull requests and forks can access base-branch caches. [1]
  • GitHub's cache documentation says pull-request runs can restore caches from the current branch, base branch, and default branch, subject to scope restrictions. [1]
  • A GitHub Security Lab advisory on Astro described leaked cache-related credentials as a path to GitHub Actions cache poisoning and release poisoning. [3]

Cache speed becomes risk when restored files are trusted

Bottom line: Treat every dependency cache as reusable build input. Use narrow keys, avoid caching secrets, separate untrusted pull-request jobs from release jobs, and verify dependency state before publishing artifacts.

GitHub Actions dependency caching restores files into a workflow before later steps run. GitHub documents the cache lookup order as exact key, partial key matches, and then ordered restore keys when configured. [1]

That behavior is valuable for package managers, compilers, and build tools. It also means the cache can influence which dependency tree, compiled output, or tool state a later step sees. A cache audit should focus on where restored files flow next.

A cache is build input, not disposable runner clutter

GitHub says a cache miss automatically creates a new cache with the configured key and path when the job completes successfully. [1] It also says an existing cache cannot be changed; a workflow must create a new cache with a new key instead. [1]

That immutability helps, but it does not remove the need to choose keys carefully. A broad restore key can still select the most recent partial match, and GitHub's reference says restore keys are checked sequentially for partial matches. [1]

Clean verdict: Cache package-manager download stores freely; be much stricter with paths that contain installed dependencies, generated scripts, compiled tools, or publish inputs.

Cache keys should include the files that define dependencies

GitHub's Node.js example builds a cache key from the runner operating system and a SHA-256 hash of package-lock.json. [1] The useful pattern is not the language; it is tying a cache to the lockfile or manifest that defines what should be restored.

For release workflows, avoid a generic key such as linux-build or a restore chain that falls back to an old dependency tree without a lockfile match. GitHub says partial restore-key matches return the most recently created cache when multiple matches exist. [1]

Use separate key namespaces for test, benchmark, preview, and release jobs. A release job should not reuse a cache written by a workflow that runs untrusted code, comments, labels, or manual inputs unless the dependency manager revalidates the result from a lockfile.

Fork and branch access changes the cache threat model

GitHub documents that workflow runs can restore caches from the current branch or default branch. For pull-request runs, GitHub says the workflow can also restore caches created in the base branch, including base branches of forked repositories. [1]

GitHub also says a cache created by a pull-request workflow is scoped to the pull request merge ref and cannot be restored by the base branch or by other pull requests targeting that base branch. [1] That is a helpful restriction, but it is not a reason to let privileged jobs consume loosely keyed caches.

Avoid this posture: A fork-triggered workflow installs and builds untrusted code, writes broad dependency caches, and a later release workflow restores from the same namespace without lockfile verification.

Secrets should never land inside cached paths

GitHub recommends not storing sensitive information, including access tokens or login credentials, in a cache path. Its cache reference says anyone with read access can create a pull request and access cache contents, and forks can create pull requests that access base-branch caches. [1]

GitHub's secure-use reference also recommends auditing how secrets are handled and checking that actions do not send secrets to unintended hosts or print them to logs. [2] Apply the same review to cache paths: package-manager config files, registry credentials, cloud CLIs, and generated auth files should stay out of restored directories.

Cache poisoning appears when untrusted code shapes later builds

GitHub Security Lab's Astro advisory reported workflows that checked out untrusted pull-request code and ran dependency installation commands. The advisory said the issue could leak a remote-cache token and lead to GitHub Actions cache poisoning, with possible release poisoning impact. [3]

Do not overgeneralize one advisory into a claim that every cache is dangerous. The durable lesson is narrower: when untrusted code can run before dependency or build caches are trusted elsewhere, the cache becomes part of the supply-chain boundary.

GitHub's secure-use reference recommends using intermediate environment variables for untrusted values in inline scripts and warns that compromised actions can access secrets and the GITHUB_TOKEN available to the workflow. [2] Those controls reduce the chance that an attacker can reach cache-writing or publish-adjacent steps.

A fast cache audit follows keys, paths, and consumers

Start with uses: actions/cache, package-manager cache settings, and built-in setup-action caching. Check each path, exact key, and restore-keys chain against GitHub's documented matching behavior. [1]

Then list who can write or influence the cached files: pull requests, scheduled jobs, manual dispatch inputs, reusable workflows, self-hosted runners, and third-party actions. Finally, list who consumes those files: tests, code generation, package publishing, Docker builds, deployment jobs, and artifact signing.

Run gha-guard locally so cache review sits beside pinned-action, token-permission, timeout, OIDC, reusable-workflow, runner, attestation, and shell-context checks.

This article is informational and is not a substitute for a security review of your own workflows, repositories, or cloud accounts.

CI Tripwire has not commissioned independent expert review of this article. Read more about the organization byline at contributors and the source posture at sourcing.

Corrections can be routed through the corrections note. Sources: 3 entries, primary platform documentation and GitHub Security Lab, last reviewed 2026-06-08.

Sources

  1. GitHub Docs, Dependency caching reference.
  2. GitHub Docs, Secure use reference.
  3. GitHub Security Lab, GHSL-2024-148_GHSL-2024-149: Code Injection and Execution of Untrusted Code in Astro's Actions workflows.