← Back to Actions Injection Check
Reading a workflow file with both patterns already in mind -- a pull_request_target
or workflow_run trigger checking out an untrusted head, and a run: step
interpolating an attacker-controlled ${{ github.event.* }} value -- genuinely works,
and it is the method most people already use. This page names no product or company being
compared against: "reading the workflow by hand" is a generic manual habit, not a competing tool.
| Case | Reading the workflow by hand | This tool |
|---|---|---|
| An untrusted checkout ref sits a few lines below the trigger, in an otherwise short workflow | Correctly caught by a reviewer who already knows the pattern | Correctly caught the same way |
The same ref buried many steps into a long job, under a nested with: block | Easy to skip once attention has moved past the trigger line -- nothing highlights the connection | Tracks the trigger once and matches any checkout ref against it regardless of distance |
Deciding whether a specific github.event.* path carries attacker content | Requires memorizing or looking up which suffixes (title, body, ref, head_ref, …) are untrusted -- getting it wrong either over-flags a safe field or misses an unsafe one | Checks every path found against the same fixed, documented suffix list every time |
An untrusted expression on line eight of a twelve-line run: | block | Easy to under-review once the first few lines look ordinary | Scans every line inside the block at its own indentation, not just the first |
An expression embedded inside format() or a comparison rather than standing alone | Easy to miss if scanning for a bare ${{ github.event... }} only | Matches the context path wherever it occurs in the line, not only as a standalone expression |
| Checking every workflow file in a repository on every dependency-bump PR | Manual, and the first thing skipped under time pressure | CLI scans a whole checkout in one pass; --fail-on-risk blocks CI, SARIF output attaches findings to the PR diff |
The pwn-request pattern is not one dangerous line, it is a relationship between two lines that
can be far apart: a trigger declaration at the top of the file, and a checkout ref:
possibly dozens of lines later inside a job's steps. A reviewer has to hold the trigger in mind
while reading every subsequent step to notice the connection, and a workflow with several jobs
and many steps makes that easy to lose. This tool notes the trigger once when it scans the file
and then checks every subsequent checkout step's ref: against it, so the distance
between the two lines never matters.
Not every github.event.* path is attacker-controlled -- github.event.repository.name
is set by GitHub, github.event.pull_request.title is text the PR's author wrote. GitHub's
own guidance names the specific suffixes that carry untrusted content, and a reviewer without that
list memorized either has to look it up every time or guess. This tool checks every matched path's
suffix against that same fixed list, so the answer does not depend on what a reviewer happens to
remember that day.
This tool has no opinion on whether a workflow should use pull_request_target at all --
it has legitimate uses, such as labeling a PR based on its base branch. Deciding whether the trigger
is warranted, and catching variants this tool does not attempt (a hand-rolled git checkout
instead of actions/checkout, or an expression built by unusual string concatenation), is
still a job for reading the workflow yourself.
A careful reviewer who knows GitHub's own guidance can spot both patterns this tool checks: a pull_request_target or workflow_run workflow that checks out an untrusted head, and a run: step that drops an attacker-controlled ${{ github.event.* }} value straight into a shell command. That review genuinely works when the file is short and the reviewer already has both patterns in mind -- the same two patterns this tool's classify.mjs looks for.
Spotting it requires remembering that exactly two triggers -- pull_request_target and workflow_run -- run with the base repository's secrets even though a stranger's fork raised the event, and then tracing forward from that trigger line to a checkout step's ref:, which can sit many lines later inside a with: block nested under a job with several other steps. A reviewer skimming for anything that looks obviously dangerous has no reason to single out an ordinary-looking actions/checkout@v4 step unless they already made that trigger-to-checkout connection at the top of the file. This tool tracks the connection mechanically: it notes the trigger once, then flags any checkout ref matching the untrusted-head pattern regardless of how many steps or lines separate the two.
Not every github.event.* path carries attacker content -- github.event.repository.name is set by GitHub itself, while github.event.pull_request.title is text the pull request's author wrote. GitHub's own script-injection guidance names the specific suffixes that carry untrusted content (title, body, message, label, ref, head_ref, and a handful more); a reviewer has to either memorize that list or look it up every time, and getting it wrong runs in both directions -- flagging a safe field wastes review time, missing an unsafe one is the actual injection. This tool checks every github.event.* path found anywhere in a run: step, including inside format() calls and comparisons, against that same fixed suffix list every time.
A run: step written as a YAML block scalar (run: |) can run many lines of shell, and an untrusted expression on line eight of a twelve-line script is easy to miss once a reviewer's attention has settled after the first few, ordinary-looking lines. This tool scans every line inside the block at the block's own indentation, not just its first line, so a path buried on the last line is caught the same as one on the first.
No. pull_request_target has legitimate uses -- commenting on a PR with a label based on the base branch, for instance -- and this tool has no opinion on whether a given workflow should use it. It only flags the specific combination of that trigger with an untrusted checkout ref, or an untrusted value spliced into a shell command. Deciding whether the trigger itself is warranted, and catching variants this tool does not attempt (a hand-rolled git checkout instead of actions/checkout, an expression built by unusual string concatenation), is still a job for reading the workflow yourself.