CriticalSupply chainPatch available

@subql/common 5.8.3 on npm: release workflow swapped in a credential stealer that runs on install and on import

@subql/common 5.8.3 stole CI, cloud, Vault and Kubernetes secrets on install and on import. Pin 5.8.2, block ci-artifacts.dev, rotate secrets.

Published
Updated
Ecosystem
npmCI/CDCloud
Fixed in
5.8.2 (last clean). 5.8.3 is no longer on npm and the latest tag is back on 5.8.2
Exploitation
Yes. 5.8.3 was published as latest on 5 October 2026; no specific victims have been confirmed

What happened

On 5 October 2026 an attacker used the release pipeline of the SubQuery project to publish a malicious @subql/common@5.8.3. All times UTC, from StepSecurity:

  • 11:23: branch chore/ci-audit-55 was created under the account ianhe8x, and commit 34128fd8cdc9 replaced the publish workflow with a short manual “canary” job.
  • 11:24: 5.8.3-onf-rt1 was published with the dist-tag redteam. It has the same application code as 5.8.2.
  • 11:52: the branch was recreated. Commit 506863d6fb82 (“[release] 5.8.3”) added a step that downloads https://ci-artifacts.dev/pkg/@subql-common-5.8.3.tgz and extracts it over packages/common before “Publish Common” runs, with no hash or signature check.
  • 11:56: @subql/common@5.8.3 was published and became latest.

A researcher’s report on the SubQuery repository notes it went out through the project’s npm trusted publisher (GitHub Actions OIDC) (subquery/subql issue 3047). The maintainer replied on 10 October that the compromised token had been deleted. The npm registry now lists 5.8.2 as latest and no longer serves 5.8.3 (npm registry). The malware records are MAL-2026-17571 and GHSA-9333-3c4x-x3h5.

StepSecurity found 19 latest @subql packages with a path to 5.8.3: @subql/cli@6.6.3 and @subql/node-core@19.3.1 (~5.8.2), @subql/query@2.25.0 (~5.8.1), @subql/node-ethereum@6.5.0 (^5.8.2), and indirectly @subql/node@6.4.6, @subql/node-solana@6.3.5 and @subql/node-starknet@6.1.0. Those parents were not compromised themselves.

What the payload does

All details from StepSecurity.

  1. Runs twice over. package.json adds "postinstall": "node ./dist/project/readers/manifest-cache.js", and readers/index.js exports the same module, so a plain require('@subql/common') also starts it. Turning off install scripts alone does not stop it.
  2. Hides. It decodes about 83 KB of JavaScript and runs it in a detached child process with the --warm flag, so the worker outlives the parent. The shipped source map omits the loader.
  3. Steals. The full environment, gh auth token output, .npmrc, .env, AWS credentials, SSH keys, kubeconfig, wallet files and AI-tool configs such as .claude.json. On Linux GitHub runners it uses sudo python3 to read Runner.Worker memory for secrets. It reads AWS Secrets Manager and SSM Parameter Store across 17 regions, Kubernetes secrets via the service-account token, and HashiCorp Vault KV mounts.
  4. Spreads through GitHub. With a ghp_ or gho_ token that has workflow scope, it pushes branch dependabot/github_actions/format/setup-formatter with .github/workflows/codeql_analysis.yml (commit “Add CodeQL Analysis”, committer github-advanced-security[bot]). That workflow dumps toJSON(secrets) into an artifact named format-results, then the payload tries to delete the run and branch.
  5. Calls home to ci-artifacts.dev on /router, beacons every 10 to 20 seconds, and accepts run and shell commands, giving the attacker a remote shell.

Am I affected?

bash
# Lockfiles: the exact bad version
grep -nE '@subql/common@5\.8\.3|common-5\.8\.3\.tgz' package-lock.json yarn.lock pnpm-lock.yaml bun.lock 2>/dev/null
grep -n -A2 '"node_modules/@subql/common"' package-lock.json 2>/dev/null | grep '"version": "5.8.3"'

# The resolved tree
npm ls @subql/common --all 2>/dev/null | grep 5.8.3

# The loader file left on disk
find / -path '*node_modules/@subql/common/dist/project/readers/manifest-cache.js' 2>/dev/null

The clean 5.8.2 has no manifest-cache.js, so any hit is the payload. Its SHA-256 is f0c8b0cde86b98a2869a22fd43ffcf61f1dcca252729e2be291e590e3dc5f49a, and the tarball’s is 031267ee37c5a84c25cb0542cbfeb49f30d5604305b0bdccdeafbedcbbe6849b.

Then look for the running worker and its lock file:

bash
ps aux | grep -E 'manifest-cache\.js|--warm' | grep -v grep
ls -la "${TMPDIR:-/tmp}"/tmp.ts018051808.lock 2>/dev/null

A missing lock file does not prove the worker stopped. In GitHub, look for the spread step in every repository the stolen token could push to:

bash
gh api "repos/OWNER/REPO/branches?per_page=100" --jq '.[].name' | grep 'setup-formatter'
gh api "repos/OWNER/REPO/actions/artifacts?per_page=100" --jq '.artifacts[] | select(.name=="format-results") | [.id, .created_at] | @tsv'

Search DNS and proxy logs for ci-artifacts.dev, and CloudTrail for bulk GetSecretValue and GetParameter calls from CI identities after 11:56 UTC on 5 October 2026.

What to do now

  1. Isolate and rebuild any host or runner that installed or imported 5.8.3. Capture the process list and network state first. Uninstalling does not stop the detached worker (StepSecurity).
  2. Rotate what the process could reach: CI secrets, cloud keys, GitHub and npm tokens, SSH keys, and Kubernetes and Vault credentials. Review where they were used since 5 October (StepSecurity).
  3. Pin @subql/common to 5.8.2 with an overrides (npm), resolutions (yarn) or pnpm.overrides entry, since the parent packages allow 5.8.3 through ~ and ^ ranges. Commit the lockfile.
  4. Block ci-artifacts.dev at your DNS or egress proxy.
  5. Delete the spread artifacts if found: branch dependabot/github_actions/format/setup-formatter, file .github/workflows/codeql_analysis.yml on that branch, and any format-results artifact. Then rotate every Actions secret in those repositories, because the artifact holds all of them.

Why this matters

This release came from the project’s own trusted-publishing pipeline, so provenance checks would have passed. The malicious step lived only on a short-lived branch that was deleted within two minutes, and main never changed. Reviewing the repository would not have shown it.

Sources