- Issues
- ISSUE-705
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.
# .github/workflows/release.yml - ❌ Unscoped key on a release jobon: [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# .github/workflows/release.yml - ✅ Key woven with the release refon: [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-keysprefix fallback must be release-scoped too - a scopedkeywith an unscopedrestore-keysis still poisonable. - Release context = a
releasetrigger, a publish action (publishActions), or a publish command in a script (publishScriptPatterns). cacheActionscarries per-action semantics:always,default(off viadisableInput/disableValue), oropt-in(on viaenableInput). Add your org's cache actions there - nothing is hardcoded.allowedJobsis 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 theircache:input is set;actions/cachealways restores, so a boguscache: falseon it does not exempt it.- The rule reasons per trigger: job-level and step-level
if:conditions and enable/disable inputs of the shapegithub.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.