The wfx stealer ran in at least 1,806 repositories this week, then deleted its own workflow runs. Anything a stolen push token can write, it can erase, and the run log is something it can write. The record that survives is the one held by something the token can't touch: the run counter, the org audit log, your cloud's own logs.
What Happened
From November 10, 12:45 UTC, 4,837 GitHub accounts opened probe pull requests in 61,764 repositories, per Aikido: a branch called wfx-docs, a title of "docs: dependency audit notes", one markdown file. It's a write-access test. Where the token could push, a commit followed. SafeDep counts 8,093 repos with malicious commits: 7,309 injected into an existing workflow, 771 into a postinstall hook, 16 as a new workflow file.
The payload is about 1,500 lines of shell, in the main variant added to a workflow that already exists. It gets toJSON(secrets) passed in, reads the runner's memory through /proc/<pid>/mem, and mints OIDC tokens for AWS, GCP and Azure, according to StepSecurity. Then it cleans up. In one repo StepSecurity traced, the inject landed at 09:17:23 UTC on November 11, the revert came 12 seconds later, and run #1964 is gone. How the tokens were stolen is still unconfirmed.
In October I wrote that in CI, push is execute, after Gheist pulled its logs and deleted its runs the same way. wfx is the same move, tried on more than 60,000 repos. This post is about the morning after.
The Log Is in the Blast Radius
GitHub's docs say deleting run logs needs write access to the repository, and the REST API deletes whole runs for anyone with write. Every commit in this campaign came from a token that had passed the probe, so every one had write.
So the evidence sat inside the attacker's write scope, next to the secrets. This isn't a wfx trick, it's the default: the job's log is owned by the repo, and the repo is what was stolen.
What the token can't rewrite is the counter. GitHub's variables reference defines the run number as "a unique number for each run of a particular workflow in a repository", starting at 1 and incrementing with each new run. Delete run #1964 and #1963 still sits next to #1965. The Hacker News: "A missing run number on the attacker's commit is the more reliable sign."
The Check
List the runs since the campaign started and print the holes:
repo=acme/api
|
|
Any output line is a run someone deleted. Take the time around it and look at that workflow file's commits in the same window. If a commit landed and was reverted within minutes, assume every secret that workflow could see is gone, and rotate. Rotation alone doesn't clean up, though: StepSecurity describes the script rewriting AWS role trust policies and adding Azure federated credentials named wfx-ci-<n>, and those survive a new secret. The created filter caps the API at 1,000 runs, which is plenty for two days on most repos.
Then look where GitHub doesn't reach. The run is deleted, but what it did with the OIDC token isn't. AWS logs AssumeRoleWithWebIdentity, the call a workflow makes to trade its token for a role:
Every event there should match a deploy you can name. One that doesn't, in the minutes around a missing run, is most likely the attacker using your role. Run it in each region you use, and do the same for CreateAccessKey and UpdateAssumeRolePolicy. A trust policy changed during the window is persistence, not a leak.
Own the Record
The deeper fix is deciding, before an incident, where the record of your CI lives. As I see it, supply chain failures come down to agency, and here it shows: most teams let the repo hold its own evidence because that's where it lands by default.
- The org audit log. It records
workflows.delete_workflow_runwith the actor, run number and commit, and a repo token can't erase it. Alert on it, and expect some noise. - The cloud side. CloudTrail for AWS, and the equivalents for GCP and Azure, kept somewhere the CI role can't write.
- Egress from the runner. StepSecurity's advice is an allowlist that blocks bare IPs and
api.telegram.org, which wfx switched to on November 12. Logged off the runner, that's a record of where the job talked, written as it ran.
And treat the probe as the breach. Aikido's guidance is that any account that opened a wfx-docs PR is compromised, whether or not the stealer ran. Cycode points out that closing the PR doesn't revoke the token.
The Tradeoff
A gap is a lead, not a verdict. People delete runs by hand, and some teams script it, so expect noise in a busy repo. The check only sees holes between two surviving runs: if the deleted run was the newest one, or the first in the window, there's no neighbour to give it away. It also misses the new-workflow variant entirely: a workflow whose only run was deleted leaves no gap to find. SafeDep counts 16 of those, so it's a small hole, but a real one. lookup-events covers 90 days in one region per call. The audit log's run events have historically been an Enterprise feature, so check your plan before you count on the alert.
None of these stops the theft. That's still the October post: environments, scoped OIDC sub, review on workflow files. This is what you need when that fails, and at 4,837 accounts it failed for a lot of people.
Look for the missing numbers today, while the runs that did survive are still there to compare against.