DevLazar
Note
074
Series
The Wire · Tooling
Date
Nov 12, 2026
Read
7 min · 1,325 words
Rev
1
Drawn by
L. Stanković · Niš
Lab notes / The Wire

The Commit That Wasn't There: Inside the wfx Raid on GitHub Actions Secrets

Since November 10, hijacked GitHub accounts have pushed a CI secret stealer to default branches and reverted it within seconds. How it hid, where the counts disagree, and what to check today.

Lazar StankovićEngineer, DevLazar · Niš, Serbia

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+.

Column chart: 61,764 repositories probed by wfx against at least 1,806 where the payload ran, per Aikido Security
Chart: wfx campaign, Nov 10 to 11, 2026: repositories reached, by stage. Source: Aikido Security.

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):

Listing 1Bash
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:

Listing 2Bash
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:

Listing 3Bash
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:

Listing 4YAML
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:

Listing 5JSON
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 misses postinstall, 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.

Lazar Stanković
Engineer and writer, running a one-man shop in Niš, Serbia. I build systems that survive their launch and write down what the work teaches.
Related notes
Note 041 · The Wire · 7 min
Valid Provenance, Poisoned Source: Shai-Hulud Reaches the tensorlake SDK
A Shai-Hulud worm shipped on October 8 in tensorlake 0.5.144, built by the project's own release workflow and carrying valid npm provenance. Provenance says where code was built, not whether its source was clean.
Note 075 · The Wire · 7 min
Sixteen CVEs in the Plumbing: Node.js Patches Its HTTP/2 and TLS Stack
Node.js 26.13.1, 24.22.3 and 22.23.5 shipped November 11 with 16 fixes, seven rated High, led by an HTTPS heap leak and an HTTP/2 crash. Who is exposed, who a proxy shields, and how to upgrade.
Note 072 · The Wire · 16 min
Vercel, Netlify, Cloudflare, Coolify, Dokploy: The Limits, the Meters and the Monthly Bill
CPU caps, memory ceilings, build limits and egress rates for five deploy targets, from their own docs as of November 11, 2026, plus two worked monthly bills. The meters differ more than the sticker prices.
← Note 073 · DeepMind Ships a Factory Robot Brain and a Cell-Aware Genome Model, and Only One Has an APINote 075 · Sixteen CVEs in the Plumbing: Node.js Patches Its HTTP/2 and TLS Stack →
The Wire · Weekly · Free
Notes like this one, every week.

AI infrastructure, shipping on a deadline, and the craft of building things that last. No noise.