← Actions Cache Poisoning Check
Actions Cache Poisoning Check vs. reviewing workflows by eye
Reading a workflow file for these two patterns is a real skill, and an experienced reviewer catches the obvious case every time. This page names no product or company -- "reviewing by eye" is the generic habit every maintainer already has, not a competing tool.
Where a careful read holds up, and where it slips
A bare actions/cache step sitting right below a pull_request_target: trigger is
easy to spot once you know the shape. Two things are harder to hold in your head while scanning a real
workflow file: actions/cache has three call shapes -- bare (reads and writes),
/save (writes only), and /restore (reads only) -- and only the first two can
actually poison anything, so a rule that flags every actions/cache string either raises a
false alarm on a read-only restore or, worse, gets tuned so loosely a real write-capable step slips past.
And restore-keys risk is not about the trigger at all: it depends on whether anything else in
the repository can write an entry under the same key prefix, which means the natural instinct -- filter for
pull_request_target or workflow_run first, then look inside -- skips over every
ordinary push workflow's cache step, which is exactly where a prefix collision is adopted.
| Case | Reviewing by eye | This tool |
|---|---|---|
Bare actions/cache step under a pull_request_target trigger | Easy to catch | Catches it the same way |
actions/cache/restore step under the same trigger | Easy to flag by mistake -- it only reads, so it cannot write a poisoned entry | Parses the step variant and applies the write-capable rule only to bare and /save |
actions/cache/save step under a fork trigger | Easy to skim past assuming "save" sounds routine, unlike a bare cache step | Treated the same as a bare step -- both write |
restore-keys on a workflow triggered by pull_request_target/workflow_run | Caught, since the reviewer is already looking at this file for fork-trigger risk | Caught the same way |
restore-keys on an ordinary push-to-main workflow | Easy to skip entirely -- nothing about the trigger looks untrusted, so the file doesn't get a second look | Flagged regardless of trigger, since the risk depends on who else can write the shared key prefix, not on what triggered the restore |
| A repository with a dozen workflow files, each with its own cache step | Re-reading all of them for both patterns, every time a workflow changes, is tedious and easy to shortcut | Paste as many files as you have, or run check.py across the whole directory |
| Blocking a newly-added finding before it merges | Depends on a human noticing it in review | check.py --fail-on-risk gives a real exit code from the same classification, wired into your own CI |
The two are complementary, not competing: this tool does not replace the judgment that decides
whether a flagged cache step is actually reachable from an attacker in your specific setup -- it replaces
the tedious, error-prone part, which is remembering which cache-action variant writes and checking every
file's restore-keys regardless of what triggered it.
Common questions
What does reviewing a workflow for cache poisoning by eye actually catch?
A reviewer who knows the pattern will catch the obvious case: a pull_request_target or workflow_run workflow with a bare actions/cache step a few lines below the trigger. That recognition is real, and this tool checks the same two conditions -- an untrusted trigger and a cache-writing step -- rather than a wider or different rule.
Why is the save/restore variant distinction easy to get wrong by eye?
actions/cache/restore only reads -- it cannot write a poisoned entry no matter what triggered the job, so flagging it would be a false alarm. A bare actions/cache or actions/cache/save step does write. A reviewer scanning quickly for the string actions/cache under a fork trigger can easily flag the restore-only step as risky, or worse, skim past a bare actions/cache step assuming it only reads because a nearby step in the same job happens to say "restore" in a comment or variable name. This tool parses the actual step reference -- actions/cache, actions/cache/save, or actions/cache/restore -- and applies the write-capable rule only to the first two.
Why is unscoped restore-keys easy to miss even on an ordinary push workflow?
The instinct when hunting for cache poisoning is to search for pull_request_target or workflow_run first, then look at what's inside. But restore-keys risk has nothing to do with which trigger fired the job doing the restoring -- it depends on whether anything else in the repository, including an unrelated and unprivileged job, can write a cache entry under the same key prefix. A reviewer who filters by trigger first will skip every ordinary push or pull_request workflow's cache step without a second look, which is exactly where an adopted prefix collision shows up. This tool flags restore-keys on any cache step regardless of trigger, because the risk lives on the restoring side, not the triggering side.
Does a clean result from this tool mean my cache usage is safe?
No. It checks two specific, well-documented patterns in one workflow file at a time. It does not catch the cache: input on actions/setup-node, actions/setup-python, actions/setup-java, or actions/setup-go, and it does not correlate a cache write in one workflow file with a restore in another -- both real ways a repository's cache usage can still be exploitable after a clean scan.