---
title: "Prevent GitHub Actions cache poisoning"
description: "Prevent untrusted workflows from poisoning caches later restored by release jobs. Separate trust domains, cache dependencies only, and verify artifacts."
url: https://starsling.dev/best-practices/github-actions/prevent-github-actions-cache-poisoning
canonicalUrl: https://starsling.dev/best-practices/github-actions/prevent-github-actions-cache-poisoning
---

# Prevent GitHub Actions cache poisoning

- [How StarSling works](https://starsling.dev/)

Best practice: Security. Last updated: 2026-08-19

Keep untrusted code from writing cache entries that trusted release or deploy jobs restore, and cache inert dependency data rather than executable workspace state or credentials.

## Table of contents

- [Do this: keep release jobs off shared caches](#do-this)
- [Avoid this: restore untrusted state before publishing](#avoid-this)
- [Treat platform restrictions as a layer, not the design](#github-cache-trigger-restrictions)
- [Cache data, not authority](#cache-data-not-authority)
- [Why it matters](#why-it-matters)
- [When a cache crosses trust boundaries](#when-to-use)
- [Verify on your repo](#verify)
- [Trying to make GitHub Actions faster?](#speedup-guides)
- [Sources](#sources)

<a id="do-this"></a>

## Do this: keep release jobs off shared caches

Pull request jobs can restore approved dependency caches but cannot create trusted release state. Release and publish jobs either install from verified lockfiles without a shared cache or use a separate cache namespace writable only by trusted triggers. Cache paths exclude build outputs, hooks, credential files, and anything later executed without verification.

_A release job that rebuilds trusted state_

```yaml
on:
  push:
    tags:
      - "v*"

permissions:
  contents: read

jobs:
  release:
    runs-on: ubuntu-latest
    environment: release
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: 22
      - run: npm ci --ignore-scripts
      - run: npm run build
      - run: npm test
```

<a id="avoid-this"></a>

## Avoid this: restore untrusted state before publishing

An attacker who influences the shared workspace can replace executable output that a later privileged job trusts and publishes.

_A trusted dispatch writing an untrusted fork into a release cache_

```yaml
on:
  workflow_dispatch:
    inputs:
      head_repository:
        required: true
        type: string
      head_sha:
        required: true
        type: string
  push:
    tags:
      - "v*"

jobs:
  cache-untrusted-head:
    if: github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    steps:
      # workflow_dispatch can write caches, but these inputs can select a fork.
      - uses: actions/checkout@v7
        with:
          repository: ${{ inputs.head_repository }}
          ref: ${{ inputs.head_sha }}
      - uses: actions/cache@v4
        with:
          path: .
          key: shared-workspace

  release:
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/cache@v4
        with:
          path: .
          key: shared-workspace
      - run: ./dist/publish
```

<a id="github-cache-trigger-restrictions"></a>

## Treat platform restrictions as a layer, not the design

GitHub.com now gives low-trust triggers read-only access to default-branch cache scope, including `pull_request_target`, `issue_comment`, and `workflow_run`; a save attempt from those triggers fails. The architectural rule still matters for GitHub Enterprise Server, third-party cache backends, trusted triggers that fetch untrusted content, and any cache containing executable state. The [ci-secure skill](/ci-secure) reports candidate cache/trust-boundary patterns with this platform mitigation so you can distinguish a blocked GitHub.com path from a reachable residual.

<a id="cache-data-not-authority"></a>

## Cache data, not authority

Keep private keys, cloud credentials, auth files, package-manager tokens, and whole workspaces out of cache paths. A cache is an optimization that may disappear or be replaced; a trusted job must be able to rebuild and verify its inputs from the repository and lockfile.

<a id="why-it-matters"></a>

## Why it matters

Caches outlive one job and are restored implicitly by key and scope. If an untrusted run can write executable or credential-bearing state that a trusted job restores, the attacker crosses from an unprivileged contribution into the release path without modifying the protected branch.

<a id="when-to-use"></a>

## When a cache crosses trust boundaries

**Use it when:** Review any cache restored by a release, deploy, publish, signing, or other secret-bearing job, especially when another trigger or cache backend can write the same namespace.

**Be careful when:** Do not cache whole workspaces or release outputs merely to save a short build. If a trusted job cannot validate the cache contents before executing them, rebuild instead.

<a id="verify"></a>

## Verify on your repo

Hand this prompt to your coding agent to audit and fix this practice in your own repo:

Audit GitHub Actions caches across trust boundaries. Map each cache key, scope, backend, writer, and restorer. Identify any cache an untrusted or attacker-influenceable workflow can write that a release, deploy, publish, or secret-bearing job later restores, plus cache paths containing whole workspaces, executable outputs, or credential files. Remove caches from privileged release paths when possible; otherwise create a trusted-only namespace and cache only inert dependency data. Preserve build reproducibility, show the diff, and open a PR rather than applying it blindly.

Ground these changes in the upstream docs before you edit: https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching, https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.

Prefer to check by hand:

- Map every cache key and backend to all workflows that can write or restore it, including reusable workflows and non-GitHub cache services.
- Flag any namespace writable by an untrusted or attacker-influenceable run and restored by a privileged job.
- Remove shared cache restores from release jobs or isolate trusted writers; narrow cache paths to inert dependency stores and exclude credentials and executable outputs.

## Go further

- [Install the ci-secure skill](https://starsling.dev/ci-secure) to check every workflow against the ten critical exploit-chain attack vectors.

<a id="more-best-practices"></a>

## More best practices for GitHub Actions

- [Secure pull_request_target in GitHub Actions](https://starsling.dev/best-practices/github-actions/secure-pull-request-target)
- [GitHub Actions secrets: prevent leaks and overexposure](https://starsling.dev/best-practices/github-actions/protect-github-actions-secrets)
- [GitHub Actions cache: dependencies, keys, and cache hits](https://starsling.dev/best-practices/github-actions/cache-dependencies)
- [All CI best practices](https://starsling.dev/best-practices/github-actions)

<a id="speedup-guides"></a>

## Trying to make GitHub Actions faster?

If you do not know why a run is slow yet, start with the diagnostic guide. [Let an agent find which of these applies: /ci-speedup](/ci-speedup). If your workflows are already optimized but still slow, see our guide to fast GitHub Actions and GitHub Actions runner alternatives. Building containers in CI? The Docker workflow guide covers layer caching end to end. Want to see how your configuration measures up before changing anything? CI Score grades workflow config against a pass/fail rubric of these practices. It is not a speed measurement.

- [GitHub Actions too slow](https://starsling.dev/github-actions-too-slow)
- [Fast GitHub Actions](https://starsling.dev/fast-github-actions)
- [GitHub Actions alternatives](https://starsling.dev/github-actions-alternatives)
- [GitHub Actions pricing](https://starsling.dev/github-actions-pricing)
- [Self-hosted GitHub Actions runners](https://starsling.dev/self-hosted-github-runners)
- [Docker CI on GitHub Actions](https://starsling.dev/ci/docker)
- [Install the free /ci-score skill to improve your GitHub Actions setup](https://starsling.dev/ci-score)

<a id="faq"></a>

## FAQ

### What is GitHub Actions cache poisoning?

It is a trust-boundary attack where an attacker influences a cache entry that a more privileged workflow later restores and executes or otherwise trusts.

### Does GitHub block cache poisoning automatically?

GitHub.com restricts writes from low-trust triggers to default-branch cache scope, which closes important paths. It does not cover every cache backend, Enterprise Server version, trusted trigger that runs untrusted code, or unsafe executable content, so workflows still need separation.

<a id="sources"></a>

## Sources

1. [GitHub Actions - dependency caching reference](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching)
2. [GitHub Security Lab - preventing pwn requests](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/)
