fix(cache): refuse a cache where the caller picks the ref - #40
Merged
Merged
Conversation
CodeQL reports cache poisoning in cross-platform-smoke.yml and private-static.yml: both accept a caller-supplied checkout_ref, and a step after that checkout could write a cache which later runs of a higher-trust ref restore. Today the finding is theoretical, because neither workflow caches. That is the problem. check_privileged_ref_guard.py already proves these workflows refuse a caller ref on a privileged event, and the cache contract already enforces refusals -- but only for actions that cache with no input at all. actions/setup-python caches only when asked, so nothing stopped a later edit from asking, and the safe property held by accident rather than by contract. The new rule closes that: a workflow exposing checkout_ref may not enable caching in any job. Verified in both directions -- clean on the tree as it stands, and injecting `cache: pip` into cross-platform-smoke produces exactly one finding naming the job and the action.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the real half of the four
actions/cache-poisoning/poisonable-stepfindings tracked in #39.What CodeQL sees
cross-platform-smoke.ymlandprivate-static.ymlaccept a caller-suppliedcheckout_ref, and a step after that checkout could write a cache. GitHub's Actions cache is ref-scoped with default-branch fallback, so an entry written from a lower-trust ref is restored into later, higher-trust runs.Why it is currently theoretical — and why that is the problem
Neither workflow caches today.
private-static.ymlsays so explicitly (enable-cache: false);cross-platform-smoke.ymlsimply never askedactions/setup-pythonfor one.Two contracts already cover the neighbouring risks and neither catches this:
check_privileged_ref_guard.pyproves both workflows refuse a caller ref on a privileged event — it even executes the extracted guard against an accept/reject matrix. That closes the privileged path, not the cache one.check_cache_contract.pyenforces refusals, but only requires one for actions that cache with no input at all.setup-pythoncaches only when asked, so it is invisible to that rule.So the safe property held by accident. A later edit adding
cache: pipwould have reopened the exact path CodeQL describes, silently and with every gate green.The rule
A workflow exposing
checkout_refmay not enable caching in any job. It reads the producer table already incatalog/cache-contract.yml, so it understands both shapes: an action that caches by default unless refused, and one that caches only when asked.Evidence, both directions
Unit-level behaviour also checked across five shapes:
cache: pipcaches, absent input does not,cache: falsedoes not,setup-uvwith no input caches (its default isauto),enable-cache: falsedoes not.One limitation, stated rather than implied: I could not run the repository launcher locally. It requires a
--copiesvenv on Python 3.13, and the uv-managed 3.13 build cannot produce one (ensurepipfails on a copied layout). I registered the package the same way the launcher does and executedcheck()directly. CI is the authority here, not my local run.