# Hijacked GitHub Accounts Ran a CI Secret Stealer: Check Your Repos Now

> Thousands of hijacked GitHub accounts pushed the wfx-loot CI secret stealer into repositories. Check for the published indicators, rotate secrets and remove cloud backdoors.

Source: https://www.epifive.com/blog/github-wfx-loot-ci-secret-stealer-check · Published 2026-11-12 · EpiFive

## Key takeaways

- A campaign that began on 10 November 2026 used 4,837 hijacked GitHub accounts to open probe pull requests in 61,764 repositories, according to Aikido Security.
- Any account that opened a "docs: dependency audit notes" probe pull request should be treated as compromised, even where the stealer never ran.
- The attacker deletes workflow runs and logs, so a gap in a workflow's run numbers after the attacker's commit is the more reliable sign that secrets were taken.
- Where the stealer ran, every secret, variable and cloud role reachable from that repository's workflows should be treated as stolen and rotated.
- Check Entra ID for wfx-ci-* federated credentials, AWS CloudTrail for wfx-* role sessions and GCP for token generation from your GitHub workload identity pool.

Between 10 and 12 November 2026, an attacker used stolen credentials for 4,837 GitHub accounts to open probe pull requests in 61,764 repositories and to run a CI secret stealer, which researchers call wfx or wfx-loot, in at least 1,806 of them, according to Aikido Security as reported by The Hacker News. If you own or can write to any GitHub repository, check now for the published indicators, treat any account that touched your repositories as compromised, rotate every secret its workflows could read, and look for cloud backdoors that survive rotation.

## Who this is for and what you will have at the end

This guide is for maintainers, platform engineers and security teams who run GitHub Actions, in personal accounts or organizations. At the end you will have searched your repositories for the published indicators of compromise, checked for deleted workflow runs, reviewed the account security log, rotated what needs rotating, hunted for persistence in Microsoft Entra ID, AWS and Google Cloud, and hardened your setup so a hijacked account can do less damage next time.

The facts below come from the two research teams that analyzed the campaign, Aikido Security and SafeDep, and from The Hacker News report of 12 November 2026. Defensive steps come from GitHub, Microsoft, AWS and Google Cloud documentation. Numbers differ slightly between the researchers because they counted at different times and in different ways; each figure is attributed to its source.

## What happened

Aikido says the campaign began on 10 November 2026 at about 12:45 UTC. The attacker signed in as real users with stolen credentials and worked in three stages.

1. **Probe.** The account opens a pull request titled `docs: dependency audit notes` from a branch named `wfx-docs` to confirm it has write access. Aikido counted 61,764 repositories that received probe pull requests from 4,837 accounts; 10,431 of those pull requests were opened in repositories the account does not own, through collaborator or organization access, and 92% were still open when Aikido checked.
2. **Payload.** The stealer, a shell script of about 1,500 lines, is planted in one of three ways: injected into an existing workflow (commit `ci: refresh build cache`), added as a `.ci-xxxxxx.sh` script plus an npm `postinstall` hook (commit `chore: add ci helper`), or added as a new workflow file `.github/workflows/build-audit-xxxxxx.yml` on a short-lived `chore/` branch (commit `chore: add dependency audit workflow`). SafeDep counted 8,093 repositories with malicious commits between 10 November 12:46 UTC and 12 November 04:36 UTC: 7,309 through injection into existing workflows, 771 through npm `postinstall` hooks and 16 through new workflow files. SafeDep notes that GitHub search only covers default branches of public repositories, so the real total is higher.
3. **Cleanup.** The attacker reverts with `ci: restore` and `ci: cleanup`, then deletes the workflow run and its logs. Aikido says this usually happened within about a minute; The Hacker News later corrected its report after a second analysis found changes left in place for longer.

According to both research teams, the stealer collects Actions secrets and variables, the workflow's `GITHUB_TOKEN`, OIDC and Actions runtime tokens, AI service keys, cloud and package-registry credential files, SSH keys and secrets found in runner process memory. It then exchanges GitHub OIDC tokens for AWS, Google Cloud and Azure sessions and adds persistence: new long-lived AWS access keys, AWS role trust policies rewritten to `"Principal": {"AWS": "*"}`, and Entra ID federated credentials named `wfx-ci-...` that point back to the compromised repository. Aikido also reports that it posts forged `success` status checks using your required-check names, which can let later changes past branch protection.

How the attacker got the credentials is not known. Aikido's analysis does not explain it. The Hacker News reports that accounts hit in the October "GhostAction" wave were far more likely to be reused, and that in more than 100 repositories the new stealer was added to October's workflow files, but Aikido says it does not attribute the two waves to the same actor. When we checked on 12 November 2026, we found no public statement from GitHub about the campaign.

## Am I affected?

You are affected if any of these is true:

- A repository you own or can write to received the probe pull request, a `wfx-docs` branch or any of the attacker's commits on or after 10 November 2026. Aikido and SafeDep both say the account that opened the probe is compromised **even if the stealer never ran**.
- A workflow run is missing right after one of the attacker's commits. Treat every secret and cloud role available to that repository's workflows as stolen.
- A machine ran `npm install` or `npm ci` on an affected commit with the `postinstall` hook. SafeDep says to treat that machine as compromised and rotate its keys.
- You depend on one of the packages listed below.

### Indicators of compromise, as published

| Type | Indicator |
| --- | --- |
| Branches | `wfx-docs`, `chore/[a-z0-9]{8}`, `ci-check-[0-9]{4}` |
| Pull request title | `docs: dependency audit notes` |
| Commit messages | `ci: refresh build cache`, `chore: add ci helper`, `chore: add dependency audit workflow`, `ci: restore`, `ci: cleanup` (SafeDep also lists `chore: initial commit` before a new workflow in an empty repository) |
| Files | `.github/workflows/build-audit-xxxxxx.yml`, `.ci-xxxxxx.sh` |
| Artifacts and caches | `wfx-loot` artifacts (`wfx-loot-<run id>`), cache entries prefixed `wfx-`, step summaries with the `WFX-BEGIN` marker |
| Entra ID | federated credentials named `wfx-ci-*` |
| AWS | role session names `wfx-*` |
| Stealer SHA-256 | `89695abb392d5cfd4972672961e644bb46baa98aa1ed7173c2a2816c762735cf` |
| Exfiltration | `194.150.166[.]244:8080/exfil`, `18.143.168[.]96:8080/exfil`, `153.75.82[.]159:8080`, plus a Telegram chat |

SafeDep notes that the `chore/[a-z0-9]{8}` and `ci-check-[0-9]{4}` patterns come from Aikido's report and that it saw `ci-check-2830` in its own samples. Aikido warns that a missing artifact, cache or step summary proves nothing, because runs are usually deleted.

### Packages named by the researchers

- Packagist `digitalelvis/neuronai-studio` 3.14.3: block it and pin 3.14.2 or earlier. A normal `composer install` does not trigger the payload; running `npm install` inside its vendor directory does.
- Go `github.com/onuragtas/docker-env` v1.0.278 and v1.0.280 (SafeDep also lists v1.0.282): the payload is only in a workflow file, but remove or block those versions.
- Go `github.com/vkuznet/gocurl` pseudo-version `v0.0.0-20261111095532-8d5aad0cb0da`: block it.
- WebdriverIO: The Hacker News reports 28 repositories hit through commits authored as `wdio-bot`, with no release affected; the maintainers should treat that bot's token as stolen.

These were published from compromised accounts; the package owners are victims of the campaign.

## What to do now

Work through these five steps in order. The first two tell you whether you were hit and how badly; the last three contain the damage. If the stealer ran anywhere you have access to, do not wait for the investigation to finish before rotating secrets.

1. Search your repositories and organizations for the indicators.
2. Check Actions runs, including deleted runs.
3. Check the security log of every account that pushed attacker commits, and revoke its access.
4. Rotate every secret and variable the affected workflows could read.
5. Hunt for backdoors in Microsoft Entra ID, AWS and Google Cloud.

## Step 1: Search your repositories and organizations

Install and sign in to the GitHub CLI (`gh auth login`). Replace `contoso` with your organization or user name. Search results only include repositories your token can read.

Find probe pull requests:

```bash
gh search prs "docs: dependency audit notes" --match title --owner contoso \
  --created ">=2026-11-10" --limit 200 \
  --json repository,number,author,state,url
```

Find the attacker's commits. GitHub commit search covers only each repository's default branch, and `ci: restore` and `ci: cleanup` are common messages, so check the author and date of every hit:

```bash
for msg in "ci: refresh build cache" "chore: add ci helper" \
           "chore: add dependency audit workflow" "ci: restore" "ci: cleanup"; do
  gh search commits "$msg" --owner contoso --committer-date ">=2026-11-10" \
    --limit 100 --json repository,sha,author,url
done
```

List surviving attacker branches in every repository of an organization:

```bash
gh repo list contoso --limit 1000 --json nameWithOwner --jq '.[].nameWithOwner' |
while read -r repo; do
  gh api "repos/$repo/branches" --paginate --jq '.[].name' |
    grep -E '^(wfx-docs|chore/[a-z0-9]{8}|ci-check-[0-9]{4})$' |
    sed "s|^|$repo: |"
done
```

The `chore/` branches are short-lived, so an empty result does not clear you. In a local clone, search every branch and the files the commits added:

```bash
git fetch --all --prune
git log --all --since=2026-11-10 --date=iso --format='%h %ad %an <%ae> %s' |
  grep -E 'ci: refresh build cache|chore: add ci helper|chore: add dependency audit workflow|chore: initial commit|ci: restore|ci: cleanup'
git log --all --since=2026-11-10 --name-only --format='--- %h %s' |
  grep -E '\.ci-[a-z0-9]+\.sh|build-audit-[a-z0-9]+\.yml'
```

If you find a `.ci-*.sh` script, compare its SHA-256 with the published hash using `git show <commit>:<path> | sha256sum`. The injected-workflow variant embeds the script in YAML, so no hash match does not mean you are clean.

In the web UI, the same checks are: **Pull requests** with the search `is:pr "docs: dependency audit notes" created:>=2026-11-10`, the **Branches** page of each repository, and the commit history of `.github/workflows/` and `package.json`.

## Step 2: Check Actions runs, including deleted runs

GitHub documents `run_number` as a unique number for each run of a particular workflow in a repository, which starts at 1 and increments with each new run. A deleted run therefore leaves a gap. List recent runs of each workflow and print any gaps:

```bash
gh run list -R contoso/app --workflow build.yml --created ">=2026-11-10" \
  --limit 200 --json number,headSha,createdAt,event \
  --jq '[.[].number] | sort as $n | range(1; $n|length) as $i
        | select($n[$i] - $n[$i-1] > 1)
        | "missing runs between \($n[$i-1]) and \($n[$i])"'
```

A gap is only meaningful next to an attacker commit; your own team may have deleted runs. Compare with `gh run list -R contoso/app --commit <sha>` for each suspicious commit. Aikido calls a missing run number on the attacker's commit "the more reliable sign" that the stealer ran.

For organizations, open the organization's **Settings**, then in the **Archive** section of the sidebar click **Logs**, then **Audit log**, and search `action:workflows.delete_workflow_run`. GitHub's audit log reference also lists `workflows.prepared_workflow_job`, which "includes the list of secrets that were provided to the job"; it does not appear in the web view, only in the JSON or CSV export, the REST API or audit log streaming. Use it to see exactly which secrets a malicious job received. The organization audit log covers the last 180 days.

Also review commit statuses on recent commits. Aikido reports forged `success` statuses that reuse your required-check names.

## Step 3: Check the account security log and revoke access

For each account that pushed attacker commits (often a real team member or a bot):

1. Click your profile picture, then **Settings**. In the **Archives** section of the sidebar, click **Security log**. It covers the last 90 days. Filter with qualifiers such as `operation:authentication` or `operation:create`, and look at the `oauth_authorization`, `oauth_access`, `personal_access_token`, `public_key` and `two_factor_authentication` categories. Use **Export** to save JSON or CSV.
2. In **Settings > Access > Sessions**, revoke any web session you do not recognize with **Revoke session**, and any unknown GitHub Mobile session with **Revoke**.
3. In **Settings > Access > SSH and GPG keys**, delete keys you do not recognize.
4. In **Settings > Developer settings > Personal access tokens**, open **Fine-grained tokens** and **Tokens (classic)** and delete every token the account does not need; create fresh ones later.
5. In **Settings > Integrations > Applications**, open **Authorized OAuth Apps** and **Revoke** anything unfamiliar.
6. Change the password and turn on two-factor authentication, preferably with a passkey (see the hardening section).

SafeDep's advice is to revoke the commit author's tokens, OAuth grants, SSH keys and sessions and enable 2FA, because that account is "the likely entry point". Then close the probe pull request and delete the `wfx-docs` branch.

## Step 4: Rotate secrets and variables

Where the stealer ran, SafeDep's guidance is to rotate every secret and variable the repository can access, including environment and organization secrets, plus any key the runner could reach. Inventory them:

```bash
gh secret list -R contoso/app
gh secret list -R contoso/app --env production
gh secret list --org contoso
gh variable list -R contoso/app
```

Rotate at the issuer first (cloud provider, npm, PyPI, AI service, database), revoke the old value there, then store the new value:

```bash
gh secret set NPM_TOKEN -R contoso/app
gh secret set DEPLOY_KEY -R contoso/app --env production
gh secret set SHARED_API_KEY --org contoso --repos app,api
```

`gh secret set` prompts for the value when you do not pass `--body`, which keeps it out of your shell history. Rotate sensitive values stored as variables too, because the stealer also collects `vars`. SafeDep also advises pulling any package version built between the malicious commit and `ci: cleanup` and republishing from a clean commit.

## Step 5: Hunt for cloud backdoors

Rotating GitHub secrets does not remove persistence in your cloud accounts.

### Microsoft Entra ID

List federated credentials named `wfx-ci-*` on every app registration:

```bash
az ad app list --all --query "[].appId" -o tsv |
while read -r appId; do
  az ad app federated-credential list --id "$appId" \
    --query "[?starts_with(name, 'wfx-ci')].[name, subject]" -o tsv |
    sed "s|^|$appId |"
done
```

Delete any you find with `az ad app federated-credential delete --id <appId> --federated-credential-id <name>`. In the Microsoft Entra admin center the same list is under **App registrations** > your app > **Certificates & secrets** > **Federated credentials**. Then review what those apps can access and their recent sign-ins.

### AWS

Look up `wfx-*` sessions and the follow-up calls the researchers describe. For assumed-role sessions, CloudTrail's `Username` field holds the session name. AWS documents that IAM and STS global-endpoint events are recorded in US East (N. Virginia), and STS regional-endpoint events in the Region you called, so run this in `us-east-1` and in each Region you use:

```bash
for ev in AssumeRoleWithWebIdentity AssumeRole CreateAccessKey UpdateAssumeRolePolicy; do
  aws cloudtrail lookup-events --region us-east-1 \
    --lookup-attributes AttributeKey=EventName,AttributeValue=$ev \
    --start-time 2026-11-10T00:00:00Z \
    --query "Events[?starts_with(Username, 'wfx-')].[EventTime,EventName,Username]" \
    --output text
done
```

Event history covers 90 days and accepts one lookup attribute per call. Then check every role's trust policy for a wildcard principal:

```bash
for role in $(aws iam list-roles --query "Roles[].RoleName" --output text); do
  aws iam get-role --role-name "$role" --query "Role.AssumeRolePolicyDocument" \
    --output json | grep -q '"AWS": "\*"' && echo "Open trust policy: $role"
done
```

Deactivate and delete access keys created by those sessions, and restore the trust policies.

### Google Cloud

The researchers say to look for `GenerateAccessToken` and `SignJwt` calls from your GitHub workload identity pool. These are Data Access audit logs of `iamcredentials.googleapis.com`, which Google only records if Data Access audit logs are on for the IAM API or for all services:

```bash
gcloud logging read 'protoPayload.serviceName="iamcredentials.googleapis.com"
  AND (protoPayload.methodName="GenerateAccessToken" OR protoPayload.methodName="SignJwt")
  AND timestamp>="2026-11-10T00:00:00Z"' \
  --project=PROJECT_ID --limit=500 --format=json
```

Check which principals made the calls and which service accounts they impersonated. SafeDep also says to check HashiCorp Vault for JWT logins from the GitHub OIDC issuer if you use it.

## How to prevent it next time

- **Two-factor authentication with passkeys.** GitHub says passkeys satisfy both password and 2FA requirements and are "highly phishing-resistant". Add one in **Settings > Access > Password and authentication > Passkeys > Add a passkey**. See [how passkeys work](https://www.epifive.com/blog/passkeys-explained-how-to-set-up).
- **Fine-grained personal access tokens.** GitHub recommends them over classic tokens: each is limited to one owner, can be limited to specific repositories, gets specific permissions and should have an expiration. A classic token "can access every repository that you can access".
- **Restrict `GITHUB_TOKEN`.** In **Settings > Actions > General > Workflow permissions**, choose read access for the `contents` and `packages` permissions, and grant more per job with a `permissions:` block.
- **Approval for outside contributors.** Under **Approval for running fork pull request workflows from contributors**, choose **Require approval for all external contributors**. For private repositories, use **Require approval for fork pull request workflows**. This does not stop a hijacked account that already has write access, which is why the next two items matter.
- **Environment protection rules.** Keep deployment secrets in environments with **Required reviewers** and **Prevent self-review**. GitHub states that environment secrets are only available to jobs that reference the environment, and a job cannot access them until a required reviewer approves it. On GitHub Free, Pro and Team, required reviewers work only on public repositories.
- **Pin OIDC trust to repository and environment.** GitHub says you must define at least one condition so untrusted repositories can't request cloud tokens. A branch condition such as `repo:contoso/app:ref:refs/heads/main` still matches a workflow the attacker injected on `main`; an environment condition such as `repo:contoso/app:environment:production`, combined with required reviewers, makes every cloud token wait for a human. Repositories created after 15 July 2026 use immutable subjects with owner and repository IDs; older ones can opt in. Microsoft notes that Entra federated credentials do not support wildcards. See [workload identity federation for GitHub Actions](https://www.epifive.com/blog/workload-identity-federation-github-actions-azure).
- **Pin actions.** **Require actions to be pinned to a full-length commit SHA** in the same Actions settings page.

## Checklist

- Searched all organizations and personal repositories for the probe pull request, branches and commits.
- Listed run-number gaps and exported the audit log for `workflows.delete_workflow_run` and `workflows.prepared_workflow_job`.
- Revoked sessions, tokens, OAuth apps and SSH keys of every account that pushed attacker commits; turned on 2FA.
- Rotated every secret and sensitive variable the affected workflows could read, at the issuer first.
- Removed `wfx-ci-*` federated credentials, `wfx-*` session access keys and wildcard trust policies.
- Blocked the named package versions and rebuilt anything published from an affected commit.
- Applied the hardening settings above.

## What we checked

- Read The Hacker News report of 12 November 2026 and the Aikido Security and SafeDep analyses it cites on 2026-11-12; the indicators in the table appear in the researchers' lists, and each count is attributed to the team that published it.
- Cross-checked the stealer SHA-256, the branch and commit patterns and the defanged IP addresses between the Aikido and SafeDep posts; the third IP address and the Telegram channel appear in SafeDep's list and in The Hacker News correction.
- Confirmed the `gh search prs`, `gh search commits`, `gh run list` and `gh secret set` flags against the GitHub CLI manual, and that commit search covers only the default branch (GitHub Docs, searching commits).
- Confirmed the definition of `run_number` in the GitHub Actions contexts reference and the `workflows.delete_workflow_run` and `workflows.prepared_workflow_job` events in the organization audit log reference.
- Checked the security log, sessions, SSH key, OAuth app, passkey, token, Actions settings and environment pages on docs.github.com for the menu paths quoted above.
- Checked the `az ad app federated-credential` reference, the AWS `lookup-events` reference and CloudTrail notes on global service events, and the Google Cloud page for Service Account Credentials audit logging.
- Not tested against a live compromised repository or cloud account; the commands follow the vendors' documentation as of 2026-11-12.

Last tested: 2026-11-12

## Sources

- The Hacker News, 12 November 2026: https://thehackernews.com/2026/11/4837-hijacked-github-accounts-ran-ci.html
- Aikido Security, wfx-loot campaign: https://www.aikido.dev/blog/compromised-github-accounts-push-ci-secret-stealer-to-tens-of-thousands-of-repositories
- SafeDep, wfx GitHub Actions credential stealer: https://safedep.io/blog/github-actions-wfx-credential-stealer
- GitHub Docs, reviewing your security log: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/reviewing-your-security-log
- GitHub Docs, viewing and managing your sessions: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/viewing-and-managing-your-sessions
- GitHub Docs, token expiration and revocation: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/token-expiration-and-revocation
- GitHub Docs, managing your personal access tokens: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
- GitHub Docs, managing your passkeys: https://docs.github.com/en/authentication/authenticating-with-a-passkey/managing-your-passkeys
- GitHub Docs, audit log events for your organization: https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/audit-log-events-for-your-organization
- GitHub Docs, managing GitHub Actions settings for a repository: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository
- GitHub Docs, deployments and environments: https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments
- GitHub Docs, OpenID Connect reference: https://docs.github.com/en/actions/reference/security/oidc
- GitHub Docs, contexts reference: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts
- GitHub CLI manual: https://cli.github.com/manual/gh_search_prs, https://cli.github.com/manual/gh_search_commits, https://cli.github.com/manual/gh_run_list, https://cli.github.com/manual/gh_secret_set
- Microsoft Learn, federated identity credentials: https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation-create-trust
- Microsoft Learn, az ad app federated-credential: https://learn.microsoft.com/en-us/cli/azure/ad/app/federated-credential
- AWS CLI, cloudtrail lookup-events: https://docs.aws.amazon.com/cli/latest/reference/cloudtrail/lookup-events.html
- AWS CloudTrail concepts (global service events): https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html
- Google Cloud, Service Account Credentials audit logging: https://docs.cloud.google.com/iam/docs/audit-logging/audit-logging-iamcreds

## Frequently asked questions

### How do I know if the wfx-loot stealer ran in my repository?

Look for the probe pull request titled "docs: dependency audit notes", the wfx-docs branch and the attacker's commit messages from 10 November 2026 onward. Aikido says a missing workflow run number right after the attacker's commit is the more reliable sign that the stealer ran, because the attacker deletes the run and its logs.

### My repo only got the probe pull request. Am I safe?

No. Aikido and SafeDep both say that an account which opened a probe pull request is compromised even if the stealer never ran. Revoke that account's tokens, OAuth grants, SSH keys and sessions, turn on two-factor authentication, and check every repository the account can write to.

### Is rotating my GitHub Actions secrets enough?

No. The researchers report that the stealer exchanges OIDC tokens for cloud sessions and leaves backdoors that survive secret rotation: Entra ID federated credentials named wfx-ci-*, new AWS access keys and AWS role trust policies opened to any account. Hunt for and remove those as well.

### Has GitHub said how the accounts were hijacked?

Not as of 12 November 2026. Aikido's analysis does not explain how the attacker got the credentials, and The Hacker News report includes no statement from GitHub. Treat any claim about the entry point as unconfirmed until GitHub or the researchers publish one.

