Starting around 12:45 UTC on 10 November 2026, an attacker used stolen credentials for 4,837 GitHub accounts to open probe pull requests in 61,764 repositories, then pushed a roughly 1,500-line secret stealer into the CI of at least 1,806 of them, according to Aikido Security, which named the campaign wfx-loot. The stealer dumps every GitHub Actions secret the job can see, exchanges the job's OIDC token for AWS, GCP and Azure sessions, plants cloud backdoors that survive key rotation, ships everything to a server or a Telegram chat, then reverts its commit and deletes the run. No bug in GitHub Actions was used: the attacker simply had write access. If your team uses GitHub, search your org for the indicators below today; if anything matches, treat every secret that repository could reach as stolen, and remove the cloud backdoors before you rotate anything.
I'm Kunal Vohra, a fractional CTO and a security researcher and cybersecurity advisor. Earlier this week I wrote about Claude Code leaking secrets into public repos and about the WorkOS breach. wfx-loot is the same story at a different layer: your CI pipeline is now the most valuable place in your company to steal from, because it holds the keys to everything else.
Who should care
- Any startup whose code lives on GitHub and uses Actions. Especially if your workflows deploy to AWS, GCP or Azure with OIDC, or hold API keys as repository secrets.
- Teams whose developers have long-lived personal access tokens or old OAuth grants. How the attacker got the tokens is still not established, but stolen, never-rotated GitHub credentials are the entry point in every report.
- Teams with bot accounts that can bypass branch protection. Commits pushed as a project's own bot account got past protection in the WebdriverIO case.
- Anyone who ran npm install on a compromised commit. One variant hides the stealer behind a postinstall hook in package.json, so it runs on developer laptops too, not only in CI.
- Teams with self-hosted runners. The stealer also collects cloud instance credentials, Docker state and runner registration material from the machine.
- Companies hit by October's GhostAction campaign. Accounts hit in October were far more likely to be used again this week.
What actually happened
The timeline
Aikido, StepSecurity, Semgrep, SafeDep and Endor Labs all published analyses on 12 November 2026, with coverage the same day from The Hacker News, Help Net Security and the Cloud Security Alliance. Their timeline agrees on the shape:
- 10 November, about 12:45 UTC: the first probe pull requests appear.
- 11 November: the stealer lands in hundreds of repositories. Aikido's counts were made by the afternoon of 11 November.
- 12 November, early UTC: new variants switch to sending stolen data to a Telegram chat. StepSecurity counted more than 63,000 probe PRs in GitHub search by 07:40 UTC, about 90 percent still open.
Step one: a harmless-looking probe
Each compromised account opens a pull request titled "docs: dependency audit notes" from a branch called wfx-docs, adding a two-line file at docs/wfx-audit.md. It does nothing except test whether the stolen token can write. Aikido's point here matters: any account that opened one of these PRs is compromised, even if no stealer ever ran in that repository. Of the probes, 10,431 went into repositories the account did not own, reaching them through collaborator or organisation access.
Step two: the stealer, hidden in four places
Researchers found the payload delivered four ways:
- Extra steps in an existing workflow, commit message "ci: refresh build cache", with steps named "Pass runtime" or "Restore cache" plus four random characters. This is the most common route: StepSecurity counted more than 21,000 such commits, and SafeDep found it in 7,309 repositories.
- A hidden script plus an npm hook, commit message "chore: add ci helper", adding a .ci-xxxxxx.sh file at the repo root and a postinstall hook in package.json (771 repositories per SafeDep).
- A new workflow file, commit message "chore: add dependency audit workflow", adding .github/workflows/build-audit-xxxxxx.yml on a short-lived chore/xxxxxxxx branch.
- Lines appended to a shell script CI already runs, such as update.sh, which StepSecurity and Semgrep both describe (Semgrep's example is a Nextcloud repository with a self-hosted runner).
Step three: steal, escalate, persist, erase
Once running, the script serialises every secret and variable the job can read, scans the memory of the runner processes for decrypted secrets, and collects cloud CLI credentials, Docker, Kubernetes, npm, PyPI and SSH material, .env and Terraform files, shell histories and AI provider keys (OpenAI, Anthropic, Gemini, Hugging Face and a long list of coding-assistant tools, per the CSA note). It then uses the job's OIDC token to get cloud sessions and, where it can, plants persistence: on AWS it rewrites a role's trust policy to allow any AWS account to assume it and tries to create access keys; on Azure it adds federated identity credentials named wfx-ci- followed by a number. Data leaves as one gzipped archive, sent to servers on port 8080 or to Telegram. Finally it pushes a revert ("ci: restore" or "ci: cleanup") and deletes the workflow run, so your Actions history simply skips a number. In one case Semgrep documented, the revert came twelve seconds after the stealer landed.
The packages it reached
At least 50 of the compromised accounts publish packages. So far the confirmed spillover is small: Packagist digitalelvis/neuronai-studio 3.14.3, and workflow-only changes in the Go modules github.com/onuragtas/docker-env (v1.0.278 and v1.0.280, with SafeDep also listing v1.0.282) and a pseudo-version of github.com/vkuznet/gocurl. SafeDep checked 218 packages from affected publishers and found no other malicious versions, though not every registry was covered.
Where the sources differ
The numbers move with method and time, so expect revisions. Aikido's post gives both "at least 1,806" and "at least 1,835" repositories that ran the payload; SafeDep counts 8,093 repositories with malicious commits on public default branches alone. The Hacker News corrected its first report, which said changes were reverted "within a minute"; it now says usually within minutes. Whether this is the same actor as October's GhostAction is disputed: Aikido says it does not attribute November to the same actor at this stage. StepSecurity found 173 probe PRs from 19 accounts made through the Fly.io GitHub App, but says it has no evidence of a Fly.io breach and that most activity used other credentials such as personal access tokens. None of the reports I read describes a public response from GitHub yet.
How to check your org in 15 minutes
These commands use the GitHub CLI (gh) logged in as an org member, so private repositories are included. They are my versions of the checks the researchers describe; test them on one repo first and replace your-org.
1. Search for the probe and the payload commits
ORG=your-org
gh search prs "docs: dependency audit notes" --owner "$ORG" --created ">=2026-11-10" --limit 200
for msg in "ci: refresh build cache" "chore: add ci helper" \
"chore: add dependency audit workflow" "ci: restore" "ci: cleanup"; do
echo "== $msg"
gh search commits "\"$msg\"" --owner "$ORG" --committer-date ">=2026-11-10" --limit 200
done
Also check your own and your team's personal accounts (swap --owner for each username), because a probe in someone's side project still means their token is stolen. Any hit on the probe means: that GitHub account is compromised. Any hit on a payload commit means: that repository's secrets are gone.
2. Look for gaps in run numbers
The stealer deletes its own run, so a missing run number right after one of those commits is the most reliable sign it executed.
REPO=your-org/your-repo
gh run list --repo "$REPO" --limit 300 --json number --jq '.[].number' \
| sort -n | awk 'NR>1 && $1!=prev+1 {print "gap after run", prev} {prev=$1}'
3. Look for files and branches that should not exist
git fetch --all --prune
git branch -r | grep -E 'wfx-docs|chore/[a-z0-9]{8}$|ci-check-[0-9]{4}'
git log --all --since=2026-11-10 --name-only --format='%h %an %s' \
| grep -E 'wfx-audit\.md|\.ci-[a-z0-9]{6}\.sh|build-audit-[a-z0-9]{6}\.yml'
grep -n '"postinstall"' package.json
4. Check your cloud accounts for the backdoors
This is the step people will skip, and it is the one that matters most. Rotating keys does not remove a trust policy that lets any AWS account into your role.
# AWS: roles whose trust policy allows any AWS principal
aws iam list-roles --output json \
| jq -r '.Roles[] | select((.AssumeRolePolicyDocument|tostring) | test("\"AWS\":\"\\*\"")) | .RoleName'
# AWS: suspicious key creation and trust-policy edits since 10 Nov
for ev in CreateAccessKey UpdateAssumeRolePolicy; do
aws cloudtrail lookup-events --start-time 2026-11-10T00:00:00Z \
--lookup-attributes AttributeKey=EventName,AttributeValue=$ev \
--query 'Events[].[EventTime,Username,EventName]' --output table
done
# Azure: federated credentials named wfx-ci-*
for app in $(az ad app list --all --query '[].appId' -o tsv); do
az ad app federated-credential list --id "$app" \
--query "[?starts_with(name,'wfx-ci')].{app:'$app',name:name,subject:subject}" -o tsv
done
CloudTrail's lookup only covers the last 90 days in the default event history, which is enough here. Also look for role sessions whose name starts with wfx-, and on GCP, review GenerateAccessToken and SignJwt calls from service accounts your CI uses.
If you find a hit
Do it in this order, because the attacker's persistence survives a rotation done first.
- Lock the account. Revoke the user's personal access tokens, OAuth app grants, SSH keys and sessions, force a password reset, and require phishing-resistant 2FA (passkeys or security keys). If it was a bot account, rotate whatever credential the bot runs on.
- Remove the cloud backdoors. Restore any role trust policy that allows "AWS": "", delete access keys created since 10 November that you can't explain, and delete wfx-ci- federated credentials.
- Rotate every secret the repository could read. Repository, environment and organisation secrets, deploy keys, cloud credentials, npm and PyPI tokens, and your AI provider keys. Put a spending cap on the AI keys while you do it.
- Treat developer machines that ran npm install on the bad commit as compromised. Rotate everything on them, not just the project's keys.
- Revert the injected changes properly and close the open probe PRs. Don't merge anything from a wfx-docs branch.
- Block runner egress to the reported exfiltration servers (194.150.166[.]244, 18.143.168[.]96 and 153.75.82[.]159 on port 8080) and to api.telegram.org, unless you have a real reason for CI to reach Telegram.
- Write down what the repo could reach. If customer data was within reach of those credentials, you may have notification duties, for example under India's DPDP Act. Get advice early.
What to change in your CI this week
Even if you found nothing, this campaign shows how much a single stolen token can do. These are the settings I'd change in every startup's GitHub org.
Make GITHUB_TOKEN read-only by default
In your organisation settings, set the default workflow permissions to read-only, then grant write per job only where it is needed:
permissions:
contents: read
The injected steps asked for id-token: write and statuses: write. A workflow that only reads should never have them.
Scope cloud OIDC trust to a branch or environment
An AWS role your CI assumes should trust one repository and one protected branch or environment, never a whole org or a wildcard:
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:environment:production"
}
}
Then put deploy credentials in a protected environment that needs an approval. A commit on a random branch should not be able to reach production credentials at all.
Require pull requests for everything, including bots
Use branch rulesets so nobody pushes straight to the default or release branches, and audit the bypass list. Bots that can bypass protection are exactly what this attacker used. Add a CODEOWNERS entry so changes to .github/workflows, CI shell scripts and package.json need review from someone who understands them.
Kill long-lived tokens
Move developers from classic personal access tokens to fine-grained tokens with expiry dates, review the OAuth apps and GitHub Apps installed on your org, and remove what nobody uses. If you were touched by GhostAction in October, finish that cleanup: researchers found more than 100 repositories where wfx was injected into GhostAction's leftover workflow files.
Stop install scripts where you can
Use npm ci --ignore-scripts in CI for projects that don't need lifecycle scripts, and alert when a pull request adds a new preinstall or postinstall hook.
Allowlist runner egress and keep logs off GitHub
CI jobs usually need a handful of hosts: your registry, your cloud, GitHub. Allowlist them on self-hosted runners and alert on connections to raw IP addresses. Stream GitHub audit logs to storage the attacker can't delete, because this payload deletes its own runs.
What I'd do as a CTO
I'd treat this as a token problem, not a GitHub problem. Branch protection protects branches; it does not protect you from a valid token with write access. The quotable version: your CI is only as safe as the oldest token that can push to it.
So the first job is the 15-minute check above across every org and every developer's account. The second is an afternoon on the settings: read-only GITHUB_TOKEN, OIDC scoped to environments, rulesets with no bot bypass, fine-grained tokens with expiry. The third, and the one most teams never do, is a written list of what each repository's CI can reach. If a repo's pipeline can deploy to production and read your OpenAI key, that repo deserves the same care as your production database. It is already on my startup security checklist, and this week is a good reason to do it.
I'd also look at the Node and runner side while you're in there: if your CI images still pin an old Node line, the November Node.js security release is worth folding into the same change.
If nobody on your team owns CI security, it doesn't need a full-time hire. A technical advisor retainer can run this review with your engineers, and a fractional CTO can own security, infrastructure and the rest of the platform end to end.
Frequently asked questions
What is wfx-loot? wfx-loot is the name Aikido Security gave to a campaign that began on 10 November 2026, in which an attacker used 4,837 hijacked GitHub accounts to probe 61,764 repositories and run a CI secret stealer in at least 1,806 of them. StepSecurity calls the same malware WFX Phantom.
How do I know if my GitHub repo was affected? Search your org for pull requests titled "docs: dependency audit notes" and commits titled "ci: refresh build cache", "chore: add ci helper" or "chore: add dependency audit workflow" since 10 November, and look for missing run numbers in your Actions history right after those commits.
Is this a vulnerability in GitHub Actions? No. StepSecurity says the attack exploits no vulnerability in GitHub Actions itself. The attacker used stolen credentials that had write access, so Actions did what it was told.
How did the attacker get the GitHub tokens? That has not been established. The reports point to stolen, unrotated tokens and personal access tokens, some activity through the Fly.io GitHub App (with no evidence of a Fly.io breach), and heavy overlap with accounts hit by October's GhostAction campaign.
I rotated my secrets. Am I safe? Not necessarily. The stealer can rewrite an AWS role trust policy to allow any AWS account and add Azure federated credentials named wfx-ci-*. Those survive rotation, so check for them and remove them.
Does it affect npm packages? One variant adds a postinstall hook, so anyone who ran npm install on an affected commit should treat that machine as compromised. Published-package spillover found so far is one Packagist package (digitalelvis/neuronai-studio 3.14.3) and workflow-only changes in two Go modules.
If you'd like a second pair of eyes on what your CI can reach, book a 30-minute call. Bring your GitHub org settings and the list of cloud roles your workflows assume, and we'll find the ones that can do more than they should.
Sources
- wfx-loot campaign pushes CI secret stealer to tens of thousands of repositories (Aikido Security)
- WFX Phantom: The GitHub Actions Secret Stealer That Deletes Its Tracks (StepSecurity)
- Seconds from Disaster: WFX Phantom and the CI Pipeline Nobody Was Watching (Semgrep)
- The wfx Campaign: Hijacked GitHub Accounts Harvest CI Secrets (Cloud Security Alliance)
- Attacker Used 4,837 Hijacked GitHub Accounts to Run a CI Secret Stealer in 1,806+ Repositories (The Hacker News)
- wfx-loot GitHub Actions secret stealer (Help Net Security)
Written By
Kunal Vohra
Technical Co-Founder & Fractional CTO
I've co-founded 6+ startups across India, the UAE, and the US, spanning AI, Web3, fintech, and cybersecurity. I write about the technical and strategic decisions that determine whether a startup thrives or stalls.
Comments
Loading comments…