Since November 10, someone has been logging in as real GitHub developers and emptying their CI pipelines of secrets. Aikido Security disclosed the "wfx" campaign on November 12: 4,837 compromised accounts opened probe pull requests in 61,764 repositories, and at least 1,806 repositories show signs the payload ran. Endor Labs calls it ongoing, with no takedown announced. The scale is notable; the design is the lesson. The malicious commit is built to leave no diff, no run and no log.
A minute on the default branch, then nothing
The first probe PRs appeared at 12:45 UTC on November 10, titled "docs: dependency audit notes" from a wfx-docs branch, to confirm write access. The payload followed in one of three forms: about 1,500 lines grafted onto an existing workflow ("ci: refresh build cache"), a .ci-xxxxxx.sh script behind a postinstall hook ("chore: add ci helper"), or a throwaway build-audit-xxxxxx.yml workflow. A revert, "ci: restore" or "ci: cleanup", came within seconds, and the run and its logs were deleted. Endor times it at about a minute per repository.
The entry point is unconfirmed. The first victim disclosure, at 06:33 UTC on November 11, reported a leaked GitHub CLI OAuth token, the long-lived kind kept in a plain config file. At 12:11 UTC that day, webdriverio took two "chore: add ci helper" commits under its WebdriverIO Bot account, which could bypass branch protection; a contributor flagged them within about 20 minutes. SafeDep counts 28 repositories hit through wdio-bot and found no affected package release.
The script is unobfuscated and labels itself "wfx". It reads ${{ toJSON(secrets) }} and ${{ toJSON(vars) }}, sweeps credential files and AI-provider keys, and trades OIDC tokens for AWS, GCP and Azure sessions. Then it digs in: long-lived IAM keys, an AWS trust policy rewritten to "Principal": {"AWS": "*"}, Azure federated credentials named wfx-ci-.... The loot leaves by HTTP POST to port 8080 on 194.150.166[.]244 or 18.143.168[.]96.
Why review never saw it
Review reads pull requests. The probe PR is a harmless docs change, and Aikido counts 92% still open. The payload skips review entirely: pushed to the default branch, run by the push trigger, erased by the next commit. The net diff of the sandwich is empty, and with the run deleted the Actions tab forgets too. The most reliable tell, per Aikido and Endor, is a gap in the workflow's run numbers.
Aikido also says the payload posts forged success statuses under the names of required checks, so branch protection waves later changes through. A check required by name is a string comparison, and any token with statuses: write can write the string. SafeDep saw the workflow request that permission but found the forging code switched off in every sample.
The corollary: least privilege in a workflow file does not bind whoever writes the file. A read-only default GITHUB_TOKEN is a default, not a ceiling; an injected job asks for id-token: write exactly as GitHub's OIDC examples do. The controls that hold are the ones the attacker cannot type into.
Counting a raid that erases itself
Sources agree on the probe and diverge on the damage. Aikido's GitHub search returns 61,839 probe PRs, 10,431 of them in repositories the account does not own. On execution, the same page gives "at least 1,806" with signs it ran and, elsewhere, "at least 1,835" that ran and lost credentials. Endor uses 1,806 for exfiltration and calls the 4,837 developers with stolen credentials; Help Net Security reports 1,800+.

SafeDep counts 61,879 probe PRs in 61,734 repositories from 4,833 accounts, and 8,093 repositories that received malicious commits by 04:36 UTC on November 12. That is a different stage, payloads landed rather than proven to run, and GitHub search sees only public default branches. Every number here is a floor.
Registries are the second ring: at least 50 hijacked accounts publish to npm, Go, Packagist, PyPI or crates. Confirmed bad versions are Packagist digitalelvis/neuronai-studio 3.14.3 and Go github.com/onuragtas/docker-env v1.0.278 and v1.0.280. SafeDep adds v1.0.282, a gocurl pseudo-version, a third exfiltration IP and a Telegram channel.
What to check before lunch
Hunt first. gh search sees only what your token can see (gh manual):
1gh search prs "docs: dependency audit notes" --match title --owner YOUR-ORG \ 2 --created ">=2026-11-10" --limit 200 --json repository,author,state,url 3gh search prs --head wfx-docs --owner YOUR-ORG --json repository,url
In each clone, look for the sandwich and for run-number holes; legitimate "ci: restore" commits exist, so read before you panic:
1git fetch --all --prune 2git log --all --since=2026-11-10 -E --format='%h %an %ad %s' \ 3 --grep='^(ci: (refresh build cache|restore|cleanup)|chore: add (ci helper|dependency audit workflow))' 4gh run list --workflow ci.yml --limit 500 --json number --jq '.[].number' \ 5 | sort -n | awk 'NR>1 && $1!=prev+1 {print "gap after run", prev} {prev=$1}'
In AWS, find wildcard trust policies (string form only), then trust-policy edits and new keys (list-roles, lookup-events). Lookups cover 90 days in one Region, so run them where your trail records IAM:
1aws iam list-roles --output text \ 2 --query "Roles[?AssumeRolePolicyDocument.Statement[?Principal.AWS=='*']].RoleName" 3for e in UpdateAssumeRolePolicy CreateAccessKey; do 4 aws cloudtrail lookup-events --start-time 2026-11-10T00:00:00Z \ 5 --lookup-attributes AttributeKey=EventName,AttributeValue=$e \ 6 --query 'Events[].[EventTime,Username,EventName]' --output table 7done
If the payload ran, rotation is not enough: delete the wildcard trust policies, keys created since November 10 and wfx-ci-* federated credentials, which SafeDep says survive client-secret rotation. Revoke flagged accounts' gh grants, PATs and SSH keys. Treat machines that installed or opened the Packagist version as compromised, Aikido advises. Endor adds code-owner review on .github/workflows/**, no direct pushes even for admins, checks required from specific GitHub Apps, and npm ci --ignore-scripts.
Then gate secrets where a pushed commit cannot reach: environment reviewers, scoped OIDC and full-SHA pins, which GitHub can enforce as policy since August 2025:
1permissions: 2 contents: read 3jobs: 4 deploy: 5 runs-on: ubuntu-latest 6 environment: production # required reviewers other than the pusher 7 permissions: 8 contents: read 9 id-token: write # only this job may mint an OIDC token 10 steps: 11 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
And bind the AWS role to that environment with the documented sub format:
1"Condition": { 2 "StringEquals": { 3 "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", 4 "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:environment:production" 5 } 6}
What we could not verify
- GitHub's position. No statement found; bulk token revocation would shrink the exposure.
- webdriverio's account. We could not open its PR #16444; details come from Endor and SafeDep.
- Initial access. One victim cites a leaked gh token; if malware is the vector, laptops join the clean-up.
- The counts. Vendor-reported, methods unpublished; Aikido's 1,806 and 1,835 are unreconciled. The chart's "under 3%" is our arithmetic.
- Forged checks. Aikido and Endor say they were posted; SafeDep found the code disabled.
- SHA pinning. The injected step used
actions/github-script@v7, per Endor, so the policy should have failed it. Our inference; it missespostinstall, and attackers can pin.
The 30,000-foot read
Nothing here broke GitHub. The attacker borrowed keys, and to a machine a key is a person. So the defence splits in two: what we cannot control, whether a token leaks from someone's laptop, and what we can, how much one token may do alone. Checks trusted by name, secrets any workflow can read, roles that trust a whole repository: each assumes the keyholder is who they claim. wfx bet otherwise, and won at least 1,806 times.
Watch whether GitHub answers with bulk revocation, whether the floor rises as private repositories report, and the registries, where 50 hijacked publishers sit one tag from the next poisoned release. The reverted commit is not an all-clear. It is the receipt.
Sources: Aikido Security, "wfx-loot campaign pushes CI secret stealer to tens of thousands of repositories" (original disclosure, account, repository, PR and execution counts, forged status checks, persistence, exfiltration endpoints, affected versions, Packagist advice); Endor Labs (timeline, payload variants and commit messages, gh token victim report, webdriverio sequence, injected step details, mitigations); SafeDep (independent counts, 8,093 repositories with malicious commits, wdio-bot scope, extra Go versions, disabled status-check forging, federated credentials surviving rotation); Help Net Security (secondary coverage of Aikido's figures); GitHub Docs, OIDC in AWS (trust policy conditions, environment sub format, id-token permission); GitHub Changelog, Aug 15, 2025 (SHA pinning policy fails unpinned workflows); AWS CLI, list-roles, AWS CLI, lookup-events and CloudTrail user guide (command options, 90-day per-Region window); GitHub CLI manual (gh search prs and gh run list flags, checked against gh 2.89.0 help). CAVEAT: every campaign figure is vendor-reported, and the three firms count different stages with different, partly unpublished methods; Aikido's page gives both 1,806 and 1,835 for repositories where the payload ran. SafeDep lists three exfiltration IPs and a Telegram channel where Aikido and Endor list two IPs, and adds docker-env v1.0.282 to the v1.0.278 and v1.0.280 they list. SafeDep also says the neuronai payload fires on npm install in the vendor directory, not composer install. No GitHub statement and no first-hand webdriverio account were found. Exfiltration IPs are defanged on purpose. The actions/checkout v4.2.2 SHA was checked against the official repository's tag; the JMESPath query matches only the string principal form.