Best practice · Security

Prevent GitHub Actions cache poisoning

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.

How StarSling works

AI agents open the PR

StarSling agents inspect your workflow, apply this optimization, and open a reviewable PR automatically.

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.12

A release job that rebuilds trusted state
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

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
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

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 reports candidate cache/trust-boundary patterns with this platform mitigation so you can distinguish a blocked GitHub.com path from a reachable residual.

Runs on your machine. GitHub API optional, for the impostor-SHA check and the dormancy note.

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.

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.

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.

Verify on your repo

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to audit and fix this practice in your own repo.

Prompt for your coding agent
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?

  1. Map every cache key and backend to all workflows that can write or restore it, including reusable workflows and non-GitHub cache services.

  2. Flag any namespace writable by an untrusted or attacker-influenceable run and restored by a privileged job.

  3. 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

One fix, all of them, or forever.

You have the prompt for this one practice. Here is how much further you can take it, each step doing more for you than the last.

  1. Fix this one thing

    Copy the prompt above

    Hand P14.7 / P8.3 to your coding agent and fix it in your repo today.

  2. Fix everything, once

    Install the ci-secure skill

    One prompt checks every workflow against ten critical exploit-chain attack vectors, explains each reachable path, and lets you choose focused fixes. Open source, runs locally. It is not a comprehensive security audit.

  3. Keep it fixed, forever

    Install the StarSling GitHub App

    Connect GitHub and the fixes stay applied as your CI evolves, with agents that keep inspecting your workflows and opening optimization PRs you review.

More best practices for GitHub Actions

Where to go next in the CI best-practices catalog.

All CI best practices

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. 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.

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.

Sources

1GitHub Actions · dependency caching reference (opens in new tab)

2GitHub Security Lab · preventing pwn requests (opens in new tab)

Last updated 2026-08-19