Skip to main content
ISSUE-705HighThird-party actions

Release workflow restores a cache with an unscoped key

Control:CTRL-705Release workflows must not restore an untrusted cache

📋 What is this?

A release or publish job restores a build cache whose key is not scoped to the release ref. GitHub Actions caches are shared across branches with a permissive prefix fallback, so a run on any feature branch - including an attacker's PR - can populate the same key (or a restore-keys prefix) that the release job later restores.

⚠️ Impact

Whoever can open a PR can prime the cache the trusted release build reuses, injecting compromised artefacts into the published package while nothing changes in the release commit. This is the May 2026 TanStack vector. Pinning the cache action by SHA does not help - the poisoned bytes live in the cache, not the action.

🔧 How to fix

Weave github.ref_name / github.sha into the cache key AND every restore-keys fallback, or disable caching on publish paths: cache: false on a setup-* action, the conditional form cache: ${{ github.event_name != '<publish trigger>' && '<manager>' || '' }}, or a caching step whose if: excludes the publish trigger - Plumber resolves these per trigger and stays silent when the restore and the publish can never share a run. The action/script inventory and a per-job allowlist are part of the control's configuration.

✗ BeforeA PR run populates `deps-<hash>`; the release job restores it and publishes the result.
# .github/workflows/release.yml - ❌ Unscoped key on a release job
on: [release]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/cache@v4
with:
path: ~/.npm
key: deps-${{ hashFiles('**/package-lock.json') }} # shared with every branch
- uses: JS-DevTools/npm-publish@v3
✓ AfterThe key includes `github.ref_name`, so a PR cache can't win the lookup.
# .github/workflows/release.yml - ✅ Key woven with the release ref
on: [release]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/cache@v4
with:
path: ~/.npm
key: release-${{ github.ref_name }}-${{ hashFiles('**/package-lock.json') }}
- uses: JS-DevTools/npm-publish@v3

💡 Tips

  • A restore-keys prefix fallback must be release-scoped too - a scoped key with an unscoped restore-keys is still poisonable.
  • Release context = a release trigger, a publish action (publishActions), or a publish command in a script (publishScriptPatterns).
  • cacheActions carries per-action semantics: always, default (off via disableInput/disableValue), or opt-in (on via enableInput). Add your org's cache actions there - nothing is hardcoded.
  • allowedJobs is a glob over the <workflow-file>/<job-id> name - the escape hatch for release jobs you have reviewed and accept.
  • setup-* actions only cache when their cache: input is set; actions/cache always restores, so a bogus cache: false on it does not exempt it.
  • The rule reasons per trigger: job-level and step-level if: conditions and enable/disable inputs of the shape github.event_name ==/!= '<event>' restrict a step to the events they admit, and the finding fires only when a restore and a publish can share an event. An enable expression Plumber cannot resolve reports ISSUE-717 (medium, verify manually) instead of this High.

⚙️ Configure this control

With the CLI alone: set it in your configuration file. Every field and default is in the controls configuration reference, CTRL-705.