Dev.to Security 🔐 Cybersecurity 👁 0 📖 4 min read

How to tell if the wfx CI stealer ran in your GitHub repo

First published on tt-tech.io. A colleague's GitHub account may have pushed a secret stealer into your repository this week, run it once, and removed it a few minutes later. If it ran, every secret and cloud role your w

First published on tt-tech.io.

A colleague's GitHub account may have pushed a secret stealer into your repository this week, run it once, and removed it a few minutes later. If it ran, every secret and cloud role your workflows can reach is in someone else's hands, and the run log that would tell you is gone.

Aikido Security, which first reported the campaign, counts 4,837 compromised accounts that opened test pull requests in 61,764 repositories, starting on 10 November. The payload ran in at least 1,806 of them, by their count. SafeDep found 8,093 repositories that received malicious commits between 10 November, 12:46 UTC, and 12 November, 04:36 UTC, and the campaign was still active when that search ran.

Why your usual defences did not see it

This is not a pull request from a stranger's fork. The commits are authored as real users of the repository, with their noreply email and no signature, which points to stolen tokens. In 1,343 repositories the author was not the owner but a collaborator or a bot. So the settings that guard against forks, like approving workflow runs from outside contributors, never come into play. The attacker has write access, the same as your team.

The pattern is short. First a pull request titled docs: dependency audit notes from a wfx-docs branch, to test write access. Then the payload, in one of three forms: about 1,500 lines added to an existing workflow (ci: refresh build cache), a script plus an npm postinstall hook (chore: add ci helper), or a new workflow file (chore: add dependency audit workflow). Then a revert, and the workflow run is deleted with its logs. SafeDep counted 7,309 repositories with the injected workflow and 771 with the npm hook, which also runs on a developer's laptop on npm install.

One account, four steps

  1. Probe: wfx-docs branch tests write access
  2. Payload: workflow edit or npm hook
  3. Run: secrets, tokens, cloud sessions
  4. Clean up: revert, run deleted

What it takes is wide: Actions secrets and variables, the GITHUB_TOKEN, OIDC tokens, credential files for AWS, Azure, GCP, npm, PyPI and SSH, and keys for AI tools. It then uses OIDC to request AWS, GCP and Azure sessions. Two of its moves are built to survive a rotation. On AWS it can create new access keys and rewrite a role's trust policy to "Principal": {"AWS": "*"}. On Azure it adds federated credentials named wfx-ci-… to app registrations, so the repository can still sign in after every secret is changed.

Rotating your secrets is not the end of it. Check the doors the stealer left open in your cloud.

What to do on Monday

Start with the signs. Both write-ups list the same ones: the wfx-docs branch, the probe pull request title, and the commit messages above, plus ci: restore and ci: cleanup. A revert does not remove a commit from history, so a search still finds them:

gh search prs --owner your-org "docs: dependency audit notes" in:title
git log --all --since=2026-11-10 --oneline \
  --grep="ci: refresh build cache" --grep="chore: add ci helper" \
  --grep="chore: add dependency audit workflow" --grep="ci: cleanup"

Then open the Actions history of each workflow the attacker touched. No wfx-loot artifact or WFX-BEGIN summary does not mean you are safe. A missing run number on the attacker's commit is the more reliable sign that the payload ran.

If you find anything, SafeDep's list is the one I would follow. A probe pull request alone means the account is compromised. Revoke that account's tokens, OAuth grants, SSH keys and sessions, and turn on 2FA. Rotate every secret the repository can read, including environment and organization secrets. Treat any machine that ran npm install on an affected commit as compromised.

Then the cloud. In Entra ID, look for federated credentials named wfx-ci-* you did not create. In CloudTrail, look for sessions named wfx-* followed by CreateAccessKey or UpdateAssumeRolePolicy. In GCP, look for GenerateAccessToken and SignJwt calls from your GitHub workload identity pool.

What helps after that

When your team has time for it, I would make "anyone with write access" mean less. Put deploy secrets in a GitHub environment instead of the repository. A job cannot read environment secrets until a required reviewer approves it, and the environment can be limited to certain branches. Check your plan first: on Free, Pro and Team, required reviewers only work in public repositories.

Do the same in the cloud. The provider checks the OIDC token's subject against the conditions on the role, and a subject can name a job environment, such as repo:octo-org/octo-repo:environment:prod. A role that trusts only that subject can only be reached by jobs that pass that environment's rules, not by a workflow pushed to any branch.

Last, required checks. Aikido describes code that posts fake passing statuses to clear merge rules; SafeDep found it switched off in the samples it read. Either way, any person or integration with write access can set a status. A ruleset can accept a required check only from a specific GitHub App.

A stolen account looks exactly like your colleague. Your CI should ask for more than write access.

Sources

  1. Aikido Security, 2026. wfx-loot campaign pushes CI secret stealer to tens of thousands of repositories
  2. SafeDep, 2026. wfx: GitHub Actions Credential Stealer Hits 8,000 Repos
  3. GitHub Docs. Deployments and environments
  4. GitHub Docs. OpenID Connect
  5. GitHub Docs. Available rules for rulesets

First published on tt-tech.io, also in French and Arabic.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.