Build1 publisher2 min readPublished Updated
A single rule produced 59 of 61 medium findings before its author read GitHub's docs
A cybersecurity undergraduate's taint scanner for GitHub Actions returned 61 medium-severity findings across 67 repositories. Of those, 59 came from one rule he then found was wrong about how the platform works.
The Engineer · Build desk

What happened
- ghast is a taint-analysis scanner for GitHub Actions that traces attacker-controlled data, such as an issue title or a fork's branch name, from where it enters a workflow to wherever something interprets it.
- A sweep of 67 mid-sized open-source repositories returned 61 medium-severity findings. One rule supplied 59 of them.
- The author stopped the sweep, read GitHub's documentation about what that rule claimed, and found the rule was wrong about how the platform works in the text it printed to users.
- Across the wider run, hand-verification produced zero exploitable vulnerabilities in the repositories and twelve bugs in the scanner.
- One high-severity report landed on vercel/next.js, a workflow_run pipeline with minimal permissions, SHA-pinned actions, a sparse checkout of one script, and artifacts written to runner.temp.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A band where one rule supplies 59 of 61 entries leaves the reviewer with an unsorted queue, so the medium queue describes ordinary CI practice with a severity label on it.
- decision Grouping findings by rule ID before reading any of them is the cheapest check available on a scanner you did not write, and it takes one pass over the output.
- cost Verification is the expensive half of shipping a scanner: by his own title the author spent longer disproving the tool than building it, and each high finding had to be read against the repository's source.
- capability The four-hop, two-job trace is the part a line-by-line reviewer cannot do, so the taint engine is worth keeping even with twelve rule bugs above it.
Two of the twelve bugs came from collapsing two questions into one. In rtk-ai/rtk, ghast reported a high-severity execution sink for the line `ENTRY="- ${PR_TITLE} [#${PR_NUMBER}](${PR_URL})"`, where the value arrives through `env:` and every read is quoted [9]. The command-position detector saw `ENTRY="-` followed by a space, matched it against a pattern for one or more `NAME=value` assignments, and decided the variable sat where the shell expects a command name [10]. The variable does not sit there: the unterminated double quote makes everything after it one word, and command position is only reachable for an unquoted read [10]. Underneath, the detector had folded the eval-context question and the command-position question into a single boolean, and the two have different answers: `eval "$V"` is execution regardless of quoting, while `$V` in command position is execution only when bare [11].
The second collapse was in the rule that answered whether a job executes the checked-out tree. That rule was a substring match against a list containing `"npm install"` [12]. A generated workflow in elastic/kibana installs a pinned CLI after checking out a pull request head, and it tripped the match [12]. Whether a package manager hands control to the working directory depends entirely on its arguments: `npm ci` runs whatever lifecycle scripts the tree's `package.json` declares, and `npm install -g @scope/[email protected]` fetches a named package from a registry and never reads the tree [13].
Both findings break the constraint the author set before writing any rules, which was that the documented mitigation must never be flagged [15]. GitHub's advice is to route untrusted values through the environment and quote the read [15]. The rtk-ai/rtk line follows that advice and got flagged anyway [19].
On the concentration signal itself: 59 of 61 is 96.7 percent of the band [18], and every other rule in the tool together produced two medium findings across all 67 repositories, about one per 34 [17]. ghast's author, a cybersecurity undergraduate at the University of Adelaide, wrote that no rule is that important: a rule that accounts for 97% of a severity band is not detecting something rare, it is describing something normal and calling it dangerous [4][2]. Grouping findings by rule ID is a cheap check, and I would run it on any scanner I inherited. The check is also narrow. It caught one rule; the other eleven bugs surfaced only because every high-severity finding across roughly 1,800 workflow files in about 200 repositories was read by hand against the source [6][7][8].
The write-up sorts the twelve into four groups, and by the author's account the groups get worse as they go [22].
What to watch
- Whether ghast splits the eval-context check from the command-position check, and what the medium band looks like on a re-run of the same 67 repositories.
- Whether the remaining groups in the write-up name failure modes beyond over-broad regexes and argument-blind package-manager rules.
- Whether any CI scanner vendor publishes findings-per-rule distributions alongside its severity counts.