Skip to content

fix(cache): refuse a cache where the caller picks the ref - #40

Merged
rldyourmnd merged 1 commit into
mainfrom
fix/refuse-cache-where-the-caller-picks-the-ref
Aug 23, 2026
Merged

rldyourmnd merged 1 commit into
mainfrom
fix/refuse-cache-where-the-caller-picks-the-ref

Conversation

@rldyourmnd

Copy link
Copy Markdown
Contributor

Closes the real half of the four actions/cache-poisoning/poisonable-step findings tracked in #39.

What CodeQL sees

cross-platform-smoke.yml and private-static.yml accept a caller-supplied checkout_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.yml says so explicitly (enable-cache: false); cross-platform-smoke.yml simply never asked actions/setup-python for one.

Two contracts already cover the neighbouring risks and neither catches this:

  • check_privileged_ref_guard.py proves 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.py enforces refusals, but only requires one for actions that cache with no input at all. setup-python caches only when asked, so it is invisible to that rule.

So the safe property held by accident. A later edit adding cache: pip would have reopened the exact path CodeQL describes, silently and with every gate green.

The rule

A workflow exposing checkout_ref may not enable caching in any job. It reads the producer table already in catalog/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

clean tree                     -> 0 findings
inject cache: pip into
cross-platform-smoke.yml       -> 1 finding, naming job 'smoke' and the action
remove it                      -> 0 findings

Unit-level behaviour also checked across five shapes: cache: pip caches, absent input does not, cache: false does not, setup-uv with no input caches (its default is auto), enable-cache: false does not.

One limitation, stated rather than implied: I could not run the repository launcher locally. It requires a --copies venv on Python 3.13, and the uv-managed 3.13 build cannot produce one (ensurepip fails on a copied layout). I registered the package the same way the launcher does and executed check() directly. CI is the authority here, not my local run.

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.
@github-actions github-actions Bot added ci and removed ci labels Aug 23, 2026
@rldyourmnd
rldyourmnd merged commit b9ca9f5 into main Aug 23, 2026
28 checks passed
@rldyourmnd
rldyourmnd deleted the fix/refuse-cache-where-the-caller-picks-the-ref branch August 23, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant