@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
- 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-55was created under the accountianhe8x, and commit34128fd8cdc9replaced the publish workflow with a short manual “canary” job. - 11:24:
5.8.3-onf-rt1was published with the dist-tagredteam. 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 downloadshttps://ci-artifacts.dev/pkg/@subql-common-5.8.3.tgzand extracts it overpackages/commonbefore “Publish Common” runs, with no hash or signature check. - 11:56:
@subql/common@5.8.3was published and becamelatest.
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.
- Runs twice over.
package.jsonadds"postinstall": "node ./dist/project/readers/manifest-cache.js", andreaders/index.jsexports the same module, so a plainrequire('@subql/common')also starts it. Turning off install scripts alone does not stop it. - Hides. It decodes about 83 KB of JavaScript and runs it in a detached child process with the
--warmflag, so the worker outlives the parent. The shipped source map omits the loader. - Steals. The full environment,
gh auth tokenoutput,.npmrc,.env, AWS credentials, SSH keys, kubeconfig, wallet files and AI-tool configs such as.claude.json. On Linux GitHub runners it usessudo python3to readRunner.Workermemory 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. - Spreads through GitHub. With a
ghp_orgho_token that hasworkflowscope, it pushes branchdependabot/github_actions/format/setup-formatterwith.github/workflows/codeql_analysis.yml(commit “Add CodeQL Analysis”, committergithub-advanced-security[bot]). That workflow dumpstoJSON(secrets)into an artifact namedformat-results, then the payload tries to delete the run and branch. - Calls home to
ci-artifacts.devon/router, beacons every 10 to 20 seconds, and acceptsrunandshellcommands, giving the attacker a remote shell.
Am I affected?
# 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/nullThe 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:
ps aux | grep -E 'manifest-cache\.js|--warm' | grep -v grep
ls -la "${TMPDIR:-/tmp}"/tmp.ts018051808.lock 2>/dev/nullA 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:
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
- 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).
- 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).
- Pin
@subql/commonto5.8.2with anoverrides(npm),resolutions(yarn) orpnpm.overridesentry, since the parent packages allow 5.8.3 through~and^ranges. Commit the lockfile. - Block
ci-artifacts.devat your DNS or egress proxy. - Delete the spread artifacts if found: branch
dependabot/github_actions/format/setup-formatter, file.github/workflows/codeql_analysis.ymlon that branch, and anyformat-resultsartifact. 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.