Safeguard
Incident Analysis•

@subql/common 5.8.3: Release Workflow Published a Swapped Tarball

On 5 October 2026 a changed GitHub Actions step swapped SubQuery's built package for a downloaded tarball and published it through trusted publishing. It ran on install and on import.

Safeguard Security Team
5 min read

At 11:56 UTC on 5 October 2026, @subql/common 5.8.3 was published to npm with a credential stealer inside. According to StepSecurity and Corgea, a commit on a short-lived branch added a release step that downloaded a tarball from ci-artifacts.dev, deleted the freshly built package and published the download in its place. Corgea reports the release went out through the project's own trusted publisher. The version has since been removed, and npm's latest is back on 5.8.2.

What's affected

  • Malicious: @subql/common@5.8.3, tracked as MAL-2026-17571 and GHSA-9333-3c4x-x3h5.
  • Clean baseline: 5.8.2, published 20 November 2025 and now latest again.
  • Canary: 5.8.3-onf-rt1, published 32 minutes earlier under a redteam dist-tag. StepSecurity found no payload in it. That dist-tag still points to it.

The real reach is downstream. Several SubQuery packages use ranges that accept 5.8.3, so installs on 5 October could pull it in without anyone touching @subql/common directly. Corgea lists, among others:

PackageVersionRange
@subql/cli6.6.3~5.8.2
@subql/node-core19.3.1~5.8.2
@subql/query2.25.0~5.8.1
@subql/node-ethereum6.5.0^5.8.2
@subql/node, @subql/node-solana, @subql/node-starknet6.4.6, 6.3.5, 6.1.0indirect via @subql/node-core

StepSecurity counted 19 latest @subql package versions with a path to 5.8.3.

How it ran, per both analyses: a postinstall hook launched dist/project/readers/manifest-cache.js, and the same file was wired into the exported reader chain so that importing the package started it too. It spawned a detached worker that decoded about 83 KB of JavaScript. The code targets .npmrc, .env, SSH keys, cloud credential stores, kubeconfig, Vault tokens, AI tool configs and the output of gh auth token. On Linux GitHub Actions runners it tries to read secrets from runner memory. With a GitHub token that has workflow scope, it tries to push a branch named dependabot/github_actions/format/setup-formatter carrying .github/workflows/codeql_analysis.yml, which dumps repository secrets into an artifact. Both firms stress that these are capabilities seen in code, and neither observed a confirmed victim.

How to check if you're exposed

Check for the version anywhere in your trees and lock files:

npm ls @subql/common --all
grep -rn '@subql/common' --include=package-lock.json --include=yarn.lock --include=pnpm-lock.yaml . | grep '5\.8\.3'

On hosts and runners that installed SubQuery packages around 5 October, look for the indicators:

ls "${TMPDIR:-/tmp}"/tmp.ts018051808.lock 2>/dev/null
find / -name manifest-cache.js -path '*@subql/common*' 2>/dev/null

In your GitHub organisation, search for the branch dependabot/github_actions/format/setup-formatter, the workflow file .github/workflows/codeql_analysis.yml if you didn't create one, artifacts named format-results, and commits attributed to github-advanced-security[bot] that you can't explain. Block and search logs for ci-artifacts.dev.

What to do now

  1. Pin @subql/common to 5.8.2 and clear cached copies of 5.8.3 from local caches, CI caches and internal registries.
  2. Rebuild hosts and runners that installed or imported 5.8.3. Uninstalling the package doesn't stop a detached worker that is already running.
  3. Rotate credentials reachable from those environments: GitHub, npm, SSH, cloud, Kubernetes and Vault.
  4. Don't rely on disabling install scripts. It stops the postinstall path but not the import path.
  5. If you maintain packages, read your own release workflow today. Look for any step that downloads an artifact and publishes it, any step without a hash or signature check, and any trigger that lets a non-default branch reach the publish job. Corgea notes the commit also loosened the job guard so workflow_dispatch could reach the release path.

Why it matters

By Corgea's account, trusted publishing worked exactly as designed here. The OIDC token came from the right repository and the right workflow, and npm accepted the release. Corgea puts the lesson plainly: provenance proved the publisher was GitHub Actions, not that the published bytes came from reviewed repository state.

The attack lived entirely in the release pipeline. The two malicious commits were siblings of the main branch tip, on a branch that was created and deleted within minutes, so a review of main would show nothing. That points at three controls: restrict which branches and triggers can run publish jobs, require approval on the release environment, and never publish bytes the workflow didn't build from the checked-out commit.

Try it yourself

Open the workflow that publishes one of your packages and trace the path from checkout to publish. Write down every input that isn't the checked-out commit: downloaded artifacts, cached directories, environment variables, actions referenced by tag. Each one is a place where someone could swap what gets published without changing your source.

Click through it at your own pace, or open it full screen.

Quick check

Why didn't trusted publishing stop @subql/common 5.8.3?

The malicious release was published by the project's own workflow, so the OIDC identity was valid. The workflow had been changed to publish a downloaded tarball instead of its own build.

Could you be affected without depending on @subql/common directly?

Yes. SubQuery packages such as @subql/cli and @subql/node-core used ranges that accepted 5.8.3, so it could arrive as a transitive dependency.

Does npm ci with ignore-scripts protect you?

Only partly. The payload also started when the package was imported.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.