← 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.

Where to answer each question.
CaseReviewing by eyeThis tool
Bare actions/cache step under a pull_request_target triggerEasy to catchCatches it the same way
actions/cache/restore step under the same triggerEasy to flag by mistake -- it only reads, so it cannot write a poisoned entryParses the step variant and applies the write-capable rule only to bare and /save
actions/cache/save step under a fork triggerEasy to skim past assuming "save" sounds routine, unlike a bare cache stepTreated the same as a bare step -- both write
restore-keys on a workflow triggered by pull_request_target/workflow_runCaught, since the reviewer is already looking at this file for fork-trigger riskCaught the same way
restore-keys on an ordinary push-to-main workflowEasy to skip entirely -- nothing about the trigger looks untrusted, so the file doesn't get a second lookFlagged 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 stepRe-reading all of them for both patterns, every time a workflow changes, is tedious and easy to shortcutPaste as many files as you have, or run check.py across the whole directory
Blocking a newly-added finding before it mergesDepends on a human noticing it in reviewcheck.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.

Run the free cache poisoning check

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.