A trusted publishing pipeline is not a trusted package: lessons from SubQuery
Software supply chain security failed at the publishing step: a malicious npm release left SubQuery's own pipeline on 5 October. Here is what to change in your build controls.
What happened
On 5 October 2026, StepSecurity published an analysis of a compromised release of @subql/common, a package in the SubQuery ecosystem on npm. Version 5.8.3, which was the package's latest tag when StepSecurity collected it, contains a hidden payload. Version 5.8.2, published in November 2025, was the comparison baseline, and the inspected copy did not contain the payload.
The detail of how it was published is the lesson. According to StepSecurity's timeline, a branch was created at 11:23 UTC that replaced the project's roughly 300-line release workflow with a 26-line manually triggered job, which published a harmless-looking prerelease. A second commit, at 11:52, added a step to the release workflow that deleted the package directory and replaced it with a tarball downloaded from an external domain, with no hash or signature check, before publishing. The payload-bearing 5.8.3 reached npm at 11:56. Both commits were unsigned and carried the same named author. StepSecurity says this does not establish who controlled the account or whether the activity was authorised, and that the commits were not merged into the main branch.
The payload runs from a postinstall script and also when the package is imported, through a detached worker, so switching off install scripts does not stop it. StepSecurity's static analysis found that it attempts to collect .npmrc, .env, cloud, SSH and Kubernetes credentials, tries to read GitHub Actions runner memory for secrets, attempts to inject a workflow into repositories reachable with stolen GitHub tokens, and sends data to an external domain. These are described as attempted or static findings rather than confirmed successes.
StepSecurity reviewed 77 @subql packages and found 19 latest versions with a possible path to the bad release, 16 direct and 3 indirect. It also noted that pinning a parent package does not pin its dependencies.
Why it matters
The publishing pipeline was the attack surface. The malicious content was swapped in between the repository build and the publish step. Anything that trusts "it came from the project's own release process" is trusting the pipeline's integrity, and this case shows that integrity depends on who can edit the workflow and what it downloads.
Your build runners are a prize. The payload goes after the credentials sitting in CI environments: tokens, cloud keys and secrets held in memory. A compromised dependency in a build is often worth more to an attacker than one in production, because builds hold the keys to everything.
Indirect dependencies widen the exposure. Teams often review what they install directly and not what that package pulls in. Version ranges in intermediate packages meant a clean-looking direct dependency could resolve to the bad release.
Familiar controls have gaps. Blocking install scripts is a sensible default, but here it was not sufficient. Controls need to be layered, and none should be presented to the business as a complete answer.
This matters beyond developers. Organisations that now generate more code with AI tools also add more dependencies, faster, and often with less scrutiny of each. Our view is that speed in building software needs a matching discipline in how software is admitted to the build.
What we would do
- Commit and review lockfiles. Resolve the whole dependency tree once, pin it, and treat changes to the lockfile as code that needs review. Install from the lockfile in CI with a strict, frozen mode.
- Delay adoption of brand-new versions. A short cooling-off period before accepting a new release gives the community time to spot a compromise. Allow exceptions for urgent security fixes, with a named approver.
- Lock down the release workflow. Require review and signed commits for changes to publishing workflows, restrict who can create and delete branches that run them, and block steps that download unverified content between build and publish.
- Starve the build runner. Give CI jobs short-lived, narrowly scoped credentials, separate build from publish, and restrict outbound network access to the domains a build actually needs. Then a stolen environment yields little.
- Know what you run. Keep a current software bill of materials for each application so that, on the day an advisory lands, you can answer "do we use this version?" in minutes and not days.
- Have the incident steps ready. If a bad package has run in a pipeline, isolate and rebuild the runner and rotate everything it could reach. Write this down before you need it.
None of this requires a new tool purchase. It requires deciding who owns the build pipeline and treating it as production.
Questions we are asked
Does disabling npm install scripts protect us from this kind of package?
Not on its own. StepSecurity found that the payload in this package also starts when the package is imported, through a detached worker, so blocking postinstall scripts leaves a route open. Script blocking is still worthwhile as one layer, but it needs to sit alongside lockfile pinning, delayed adoption of new versions and network controls on build runners.
Why did pinning our direct dependency not help?
Pinning a package fixes its version, not the versions of what it depends on. StepSecurity noted that pinning a parent package does not pin its dependencies, and found ranges in other SubQuery packages that could resolve to the bad release. A committed lockfile, reviewed on change, is what fixes the whole tree.
What should we do if the affected version ran in our pipeline?
Treat the host or runner as compromised. Isolate it, preserve evidence, rebuild from a trusted image and rotate every credential the process could reach, including CI, cloud, GitHub, npm and SSH secrets. Uninstalling the package is not enough, because the payload targets credentials rather than the application itself. Review how those credentials were used afterwards.