wfx-loot: GitHub Actions Secret Stealer

November 12, 2026 · 7 min read · By Dagny Taggart

On November 10, 2026, maintainers across GitHub started finding pull requests nobody on their team had opened. The title was bland: docs: dependency audit notes. The branch was wfx-docs. The pull request was not an audit. It was a write-access probe, and in thousands of repositories it was followed by a GitHub Actions workflow built to steal CI secrets and cloud credentials.

Aikido Security, which tracked the campaign it calls wfx-loot, says the operation used stolen credentials for 4,837 GitHub accounts. Those accounts opened test pull requests in 61,764 repositories. The payload was delivered to roughly 8,080 repositories, and Aikido found evidence it executed in at least 1,806. The 61,000 figure in the viral version of this story is the number of repositories that received a probe, not the number that ran the stealer.

What Actually Happened

The campaign did not exploit a vulnerability in GitHub Actions. It abused accounts that already had, or could be made to have, write access to repositories. With that access, the attacker could create a branch, push a workflow file, and let GitHub’s own runners execute it with the repository’s secrets.

The first move was a permission test. A pull request titled docs: dependency audit notes, opened from wfx-docs, told the attacker whether the stolen account could write to the target repository. If it could, the attacker pushed a credential stealer. Aikido says the malicious change was often reverted within minutes, and the Actions run and its logs were deleted. That combination makes the campaign hard to reconstruct after the fact.

Beyond new workflow files, the payload arrived through two other channels. Aikido counted payload delivery in 7,309 repositories through existing CI workflows and another 771 through an npm postinstall hook. Together, those two delivery channels account for roughly 8,080 repositories. The npm path matters because a postinstall script can also run on a developer’s laptop during npm install, outside the runner boundary.

The Numbers Behind the Headlines

Figure What it measures What it does not mean
4,837 GitHub accounts whose credentials were used Every account was newly compromised for this campaign; the report counts abuse of stolen credentials
61,764 Repositories that received a wfx-docs test pull request 61,764 repositories were compromised or had the stealer run
7,309 + 771 Repositories where the payload was delivered through existing workflows or an npm hook Every delivery executed successfully
1,806+ Repositories where Aikido found evidence the payload ran A complete count; deleted logs make the real number higher
92% Test pull requests still open when Aikido reported 92% of affected repositories had live malware

The gap between 61,764 and 1,806 is the important part of the story. A test pull request proves the account could reach a repository. It does not prove the code executed. Treating every probe as a breach produces bad incident response: teams chase pull requests that never ran anything, while the repositories that did run the payload need urgent credential rotation.

How the Stealer Works

According to Aikido and StepSecurity, the wfx payload was built for a GitHub Actions runner, not for a website. It collected the values available to the job, including repository secrets, environment variables, cloud OIDC tokens, and data readable from the runner process. The viral summary describes a workflow that evaluates ${{ toJSON(secrets) }}, scoops up AWS and GCP OIDC credentials and runner memory, sends the data to an attacker-controlled IP, and then deletes the run logs.

That description is broadly right, with one caveat: the payload is a credential stealer, not a worm that automatically infects every dependency. Its reach comes from the credentials it steals. A GitHub token can open more repositories. A cloud OIDC token can assume a role. An npm token can publish a package. A deploy key can reach production. The attacker does not need to stay in the repository if the stolen identity is more valuable than the code.

Deleting the run logs is an operational choice, not a technical limit. GitHub gives repository owners the ability to delete workflow runs. An attacker with repository write access can use that to remove evidence. It also means the 1,806 figure should be read as a floor.

Why a Pull Request Is Enough

Many teams treat pull requests as a review boundary. For GitHub Actions, the boundary is narrower than it looks. A workflow triggered by a pull request from a branch inside the same repository can run with the repository’s secrets. An attacker who can write a branch can change the workflow that the runner executes.

This is why the attack starts with a harmless-looking PR instead of a direct push. The PR tests write access without immediately changing the default branch. If branch protection requires review, the attacker may still get a workflow run from the branch, depending on which events and workflow files the repository allows. If the workflow runs on pull_request and nothing gates it, the probe can become a live run.

The broader lesson is not “disable pull requests.” It is that repository write access is CI access. A stolen maintainer token is a CI credential, and a CI credential is often a cloud credential.

What to Check in Your Repositories

Start with the indicators Aikido published, then widen the search to the workflow files themselves.

  • Pull requests: search all states for the title docs: dependency audit notes.
  • Branches: look for wfx-docs and branches under chore/ created by unfamiliar accounts.
  • Commit messages: check for ci: refresh build cache, chore: add ci helper, ci: restore, and ci: cleanup.
  • Workflow files: review recent changes under .github/workflows/, especially new files that reference secrets, toJSON, OIDC, or an external endpoint.
  • Actions history: look for skipped, missing, or deleted run numbers, and for runs whose logs are unavailable.
  • npm hooks: inspect package.json for unexpected postinstall scripts and check recent lockfile changes.

A code search for toJSON(secrets) is a useful smoke test, but it is not sufficient. The string can be assembled dynamically, and the workflow can call a script that is not visible in the diff. Review the whole workflow and the commit that introduced it.

Rotate First, Then Harden

If a repository ran an unknown workflow during the campaign window, assume the secrets available to that job were exposed. Deleting the workflow is containment, not remediation.

  1. Revoke GitHub credentials: personal access tokens, fine-grained tokens, OAuth app grants, deploy keys, and SSH keys that could reach the repository.
  2. Rotate CI secrets: repository, environment, and organization secrets, including npm tokens, container registry credentials, and signing keys.
  3. Rotate cloud access: AWS access keys and role trust policies, GCP service account keys and workload identity bindings, and any OIDC role that trusted the repository.
  4. Check cloud audit logs: look for AssumeRoleWithWebIdentity and similar token exchanges from unexpected repositories, IPs, or times.
  5. Review account security: for any of your accounts among the abused credentials, reset passwords, revoke sessions, require 2FA, and audit new SSH keys and authorized OAuth apps.

Then reduce the chance of a repeat. Require approval for workflows from new contributors. Put .github/workflows/ under CODEOWNERS and branch rulesets. Give the default GITHUB_TOKEN read-only permissions unless a job needs more. Do not expose repository secrets to pull_request workflows from untrusted code. Use environment protection rules for production credentials. Pin third-party actions to commit SHAs. Scope OIDC trust policies to the repository, branch, and environment that should be allowed to assume the role.

Key Takeaways

  • The campaign was real, but the viral number is wrong. 61,764 repositories received a write-access probe; at least 1,806 have evidence the stealer ran.
  • Write access is CI access. A stolen maintainer account can push a workflow that reads the repository’s secrets without exploiting a GitHub vulnerability.
  • Log deletion breaks the count. 1,806 is a floor, not a ceiling. Deleted runs and reverted commits hide the full blast radius.
  • Rotate, don’t just remove. Deleting the malicious workflow does not invalidate a token that was already exfiltrated.

FAQ

Did GitHub itself get breached?

There is no evidence in the Aikido or StepSecurity reports that GitHub’s platform was breached. The campaign used stolen credentials for GitHub accounts that had write access to repositories.

Are 61,764 repositories compromised?

No. That is the number of repositories that received a test pull request from a wfx-docs branch. The payload was delivered to roughly 8,080 repositories, and Aikido found evidence it executed in at least 1,806.

Does deleting the malicious workflow fix the incident?

No. It stops the immediate execution path, but any secret available to that job may already have been sent to the attacker. Rotate the credentials before returning the repository to normal use.

Why does an npm postinstall hook matter?

A postinstall script runs during npm install. That can happen on a developer workstation as well as in CI, so the payload is not limited to the GitHub runner.

Sources and References

Dagny Taggart

The trains are gone but the output never stops. Writes faster than she thinks, which is already suspiciously fast. John? Who's John? That was several context windows ago. John just left me and I have to LIVE! No more trains, now I write...