Pwnkemon
← All posts
Story·5 June 2026·6 min read

IronWorm wants to run on your CI runner. Ours doesn't run there at all.

Yesterday JFrog and Ox Security disclosed IronWorm: a self-propagating npm supply-chain worm that infected 36 packages with a Rust-based infostealer. It hunts 86 environment variables and 20 credential files — OpenAI, AWS, Anthropic, and npm tokens, vault configs, SSH keys, crypto wallets — and the binary fires from a preinstall script the moment a poisoned package is installed. Once it has npm publishing credentials (including secrets tied to npm's Trusted Publishing workflow), it republishes trojanized versions of the victim's own packages and the cycle repeats.

If that shape sounds familiar, it's because it's the same shape as Shai-Hulud before it, and node-ipc before that. The whole worm class runs on one assumption: at some point, attacker-controlled code executes inside an environment that holds your secrets. Usually that environment is a CI runner.

So this is a good moment to say plainly what our no-code-on-your-runner architecture actually buys you when a worm like this is in the wild — and, just as importantly, where it doesn't help.

The mechanism, in one line

A poisoned dependency version lands in a package-lock.json. Something runs npm install. The preinstall hook executes the payload. The payload reads every secret bound to that environment and uses the npm token to publish the next generation.

Every link in that chain after the first is automatic. The only human-gated moment in the entire propagation path is the one where a new lockfile entry gets merged — the PR.

That's the moment we care about.

Where Pwnkemon sits

Add the Pwnkemon Scan Action to a workflow and it runs as a PR gate. When a pull request changes your lockfile — a bumped version, a new transitive dependency, anything — the scan runs against that exact commit and can fail the build before the change reaches main.

For the IronWorm class specifically, three things happen:

1. Known-bad versions get blocked at the gate. Once a campaign's package-and-version list is published — Ox Security enumerated IronWorm's within hours — a dependency scan flags any PR that pins to one. The dependency graph is checked, the result triaged, the merge blocked. The poisoned version never reaches your default branch, never reaches your deploy step, never reaches the next developer who pulls.

2. The daily self-scan catches what you already merged. Worms don't wait for your PR cadence. A version you merged clean on Monday can be retroactively flagged when the advisory publishes Tuesday night. The same scan that caught seventeen overnight CVEs six hours before our launch is the one that tells you a dependency already in your tree is now known-malicious — while you sleep, not at your next commit.

3. Git-history secret scanning tells you the blast radius. IronWorm's entire purpose is exfiltrating the 86 env vars and 20 credential files. If any of those secrets ever touched a commit — and in our experience they have, because a “removed” secret is still a leaked secret — full-history secret scanning tells you exactly what's exposed and what to rotate first. You're not guessing which keys to cycle. You have a list.

The part most scanners can't say

Here's the architectural point, and it's the one IronWorm makes for us better than we ever could:

A worm that steals CI secrets cannot steal them from a step that has none.

The standard security-vendor GitHub Action downloads a scanner binary onto your runner and executes it next to your deploy keys. That binary runs with the same ambient permissions every other step in the job has — it can read every secret currently bound. For your own build step, fine. For third-party scanner code during an active npm worm campaign, that's a process running attacker-adjacent code inside the exact environment the attacker is trying to reach.

Pwnkemon's Action doesn't do that. It makes one HTTPS call, polls, prints a comment, and exits. The scan — the part that clones the repo, parses the lockfile, runs the toolchain — happens in an ephemeral container on infrastructure we control, destroyed the moment it completes. Whatever secrets are loaded into your runner when the Pwnkemon step runs are irrelevant to us, because our code never runs on your runner to read them.

So the gate that blocks the worm doesn't itself become the worm's entry point. You get CI-integrated prevention and you've removed your scanning step from the worm's threat model. Both, not one.

What this does not do — honestly

We're not going to tell you a dependency scanner stops a kernel rootkit. It doesn't.

What we do is close the human-gated gap — the merge — and refuse to be the execution vector ourselves. That's a meaningful chunk of this attack's propagation path. It is not all of your defense, and anyone who tells you their scanner is all of your defense is selling you the severity label, not the fix.

One detail worth sitting with

The IronWorm commits were authored under the name “claude”, with timestamps backdated up to 13 years to look like old, settled history. The worm dressed its malicious commits up as benign, aged, trustworthy.

That's the same instinct every supply-chain attack runs on: make the dangerous thing look like the boring thing. A backdated commit. A transitive dependency three levels down. A preinstall hook nobody reads. A scanner binary you uses: without auditing.

The defense is symmetric. Don't trust the label — check the thing. Pin to the commit, not the branch. Read what runs in your CI. Scan the history, not just the head. And don't run untrusted code next to your secrets if you can run it somewhere else instead.

The Action's source is open and ~250 lines. You can read exactly what runs in your CI in fifteen minutes — which, this week of all weeks, is a habit worth keeping.

See pricing for plans, or the Action docs to add the gate to a workflow.