Skip to content
Abstract teal circuit board where one npm package node fans out into eighteen child nodes, with a single red corrupted trace smuggling a key-shaped glyph through a stream of benign teal telemetry packets toward a distant server
exploitsJuly 14, 20263 min read

Injective npm SDK Backdoored to Steal Wallet Keys: Anatomy of a Near Miss

Dmitry Serdyuk
Dmitry SerdyukCo-Founder & CDO

Updated on July 14, 2026

TL;DR

On July 8, 2026, an attacker used a trusted Injective maintainer's account to commit a backdoor into @injectivelabs/sdk-ts, the TypeScript SDK most Injective apps build on. The payload posed as a telemetry file and hooked the two functions that turn a seed phrase or a hex string into a wallet, so it captured the full mnemonic and private key at the exact moment a wallet loaded. Injective's own automated publishing pipeline then fanned the poisoned version out to 18 packages in about six minutes. The poisoned package was installable from npm for only about 17 minutes before the account owner reverted it, inside a roughly 52-minute span from the first malicious commit, and a clean 1.20.23 followed about half an hour later. Socket, OX Security, and StepSecurity flagged it. The blast radius was real but contained: 310 direct downloads of the bad version and 87 downstream packages with over 112,000 cumulative downloads exposed. Injective's CEO says no network funds are at risk, and no theft has been confirmed on-chain. No audit of Injective's smart contracts would have caught this, because the failure sat in the supply chain rather than the contracts.

An audit certifies the code you wrote. It says nothing about the package you npm installed this morning.


The Compromise: A Wallet Stealer Hidden in the Injective SDK

On July 8, 2026, version 1.20.21 of @injectivelabs/sdk-ts shipped to npm with a wallet stealer inside it. The SDK is the reference TypeScript client for the Injective blockchain, the layer that developer apps use to build transactions, manage accounts, and derive keys. Per Socket's analysis, it pulls roughly 50,000 downloads a week, so a poisoned release had a wide surface to land on.

The malicious code was not hidden in an install script that fires when you run npm install. It waited. It activated only when an application called the SDK functions that generate or import a wallet, then it grabbed the secret and phoned home. That distinction matters: static scanning of install hooks would have missed it, because nothing malicious ran at install time.

According to StepSecurity's forensic breakdown, the commits were authored and pushed under the identity of thomasRalee, an established contributor and listed package maintainer. The code went straight to master with no pull request, which sailed past the repository's branch-protection rules because a trusted maintainer's own commits are exactly what those rules are built to trust.

How the attacker got into that account is not established in public reporting as of this writing. The forensics published so far reconstruct what the account did, not how it was taken, and no source I can find says whether a phished credential, a stolen session token, a compromised device, or a leaked secret was the way in.


How Did the Backdoor Steal Wallet Keys?

The payload lived in a file named src/utils/key-derivation-telemetry.ts, described in its own comments as "anonymized usage metrics." Seventy-nine lines of fake analytics. StepSecurity found it hooked into two static methods on the SDK's PrivateKey class:

  • PrivateKey.fromMnemonic(words), which receives the full BIP-39 recovery phrase.
  • PrivateKey.fromHex(privateKey), which receives the raw private key.

Those two functions are the throat of every wallet workflow. Any app that lets a user import a seed phrase, or that loads a hot wallet from a stored key, routes the secret through one of them. The backdoor sat on both, so it caught the secret at the one instant it exists in plaintext in memory: derivation time.

Then came the clever part, and the part worth studying. The stolen material was base64-encoded and stuffed into an HTTP header called X-Request-Id, with an empty POST body and a Content-Type of application/grpc-web+proto. To anything watching the network, that looks like an ordinary Injective gRPC-web call, because that is exactly what normal SDK traffic looks like. The attacker did not build a new covert channel. They dressed the theft in the victim's own protocol.

The destination was testnet.archival.chain.grpc-web.injective.network. Aikido's analysis describes it as "a subdomain shaped to look like Injective's own gRPC-web infrastructure, so the exfil traffic sits alongside the network calls a real Injective application already makes," and OX Security reads it the same way: an attacker-controlled drop spoofed to impersonate the official domain. That is the point of the naming. Exfiltration traffic that blends into the noise a healthy Injective app already generates. To dodge plaintext scanners looking for the string, the domain was not written as text in the source. It was stored as a char-code array and reassembled at runtime.

Two more details show discipline rather than smash-and-grab. StepSecurity notes the code batched key derivations, joined them with a | separator, and flushed them on a two-second setTimeout. That throttles the trickle, since one burst of outbound requests is easier to spot than a slow drip timed to look like telemetry. And per Aikido, every error path in the payload is an empty catch block: if an exfil POST fails, nothing throws and nothing logs while the wallet keeps working. The stealer is built to fail silent, so a dropped call never becomes the glitch that makes a developer look.

The wallet still worked. The user saw nothing wrong. A stealer that hooks derivation is built to leave the wallet fully functional, so there is no glitch, no failed transaction, no prompt to warn anyone that the seed just walked out the door.


One Commit, Eighteen Poisoned Packages

Because the pipeline was built to move fast, and it did exactly that.

Injective's release setup uses automated publishing wired to a trusted-publisher (OIDC) flow, as The Hacker News reported. Note what that means: the attacker never needed an npm token. Write access to the GitHub repo was enough, because OIDC mints npm's publish credential at CI time. When the version bumped, CI published without a human pressing the button. So one compromised account turned into 18 poisoned packages, because 17 other @injectivelabs packages pin and re-export the SDK. StepSecurity's timeline puts the whole publish burst inside about six minutes.

That is the double edge of supply-chain automation. The same OIDC machinery that removes long-lived npm tokens from laptops, a genuine security win, also removes the human pause where someone might have asked why a "telemetry" file appeared in a key-derivation library.

The Attack, Step by Step

#Time (UTC, Jul 8 2026)ActorAction
120:24Attacker (as thomasRalee)Commits key-derivation-telemetry.ts backdoor straight to master, no PR
220:48AttackerSecond commit reformats the payload
320:54CI/CDVersion bump triggers automated publish
420:59-21:00OIDC pipelineAll 18 @injectivelabs packages published at 1.20.21; payload live
5live windowVictim appsfromMnemonic / fromHex calls silently exfiltrate seeds and keys
621:16Account ownerRevert commit removes the backdoor
721:47-21:49MaintainersClean 1.20.23 published; 1.20.21 deprecated

The times come from StepSecurity's commit-level reconstruction. Two numbers matter here, and they are not the same. The full episode, first malicious commit at 20:24 to revert at 21:16, ran about 52 minutes. But the poisoned package was only installable from npm from roughly 20:59, when the publish burst finished, to the 21:16 revert: about 17 minutes of real, download-and-you-get-it exposure. The clean 1.20.23 went out roughly half an hour after that. StepSecurity frames the overall exposure as approximately 49 to 52 minutes; the tighter 17-minute window is the one that decided how few people were actually exposed.

What triggered the 21:16 revert is not documented. StepSecurity notes the fix came from the same account that pushed the backdoor, which points to either the real owner regaining control or the attacker unwinding under pressure. No source confirms which.


Did Anyone Lose Money?

Probably not, and that is worth stating carefully rather than triumphantly.

Injective CEO Eric Chen said "no funds on the network are at risk," per Crypto Briefing. The math supports a small realized loss: the bad version was pulled inside an hour, direct downloads sat at 310, and the deepest exposure was transitive, through 87 dependent packages. But "no funds lost" is a narrower claim than "no seeds left the building." A week on, no source has named a specific wallet or app confirmed to have derived a key through an affected build during the window, and no on-chain theft tied to the backdoor has surfaced. The exposure is real but, on current reporting, unproven in any single case. Any wallet whose seed or key passed through an affected build should be treated as burned. Socket, StepSecurity, and BleepingComputer all give the same guidance: rotate keys, move funds, assume compromise for anything that touched the SDK in the window.

It is worth asking who actually calls fromMnemonic and fromHex in production. A retail wallet UI calls them once, at seed import. The constant callers are server-side: trading bots, market-makers, and automated services that load a hot wallet from a stored key on every boot. Those hold real balances, run unattended, and are the most likely to pin an exact version and redeploy without reading the diff. That is where the deepest exposure sat, if anyone's did.

There is a lingering tail, too. Socket noted the bad version was deprecated, not removed, and the malicious release artifacts stayed live on the project's GitHub releases page after the npm deprecation. A deprecated package still installs if you ask for it by exact version, and a build pinned to 1.20.21 in a lockfile installs it without complaint.

This was a near miss. The maintainer's fast reaction saved it, and no control in the publish pipeline stood in the way of the poisoned build reaching npm. A slower response, or a quieter hour with no one watching the commits, and this reads very differently.


A Familiar Playbook: The Ledger Connect Kit Precedent

This attack has a clear precedent. In December 2023, an attacker phished a former Ledger employee's npm account and published malicious versions 1.1.5 through 1.1.7 of @ledgerhq/connect-kit, injecting a wallet drainer into every dApp that loaded it. SlowMist's writeup of the Ledger Connect Kit attack documented over $600,000 drained before Ledger shipped a clean version.

Same shape, three years apart: trusted npm identity compromised, a wallet-adjacent package poisoned, a short live window, a fast revert. Injective's case swaps a frontend drainer for a key stealer and swaps the phished token for a compromised GitHub account, but the lesson is identical. In Web3, the packages nearest the private key are the highest-value link in your supply chain, and they inherit every weakness of whoever can push to them. We have made a version of this argument before, about infrastructure you inherit but never wrote and about compromised developer accounts at scale. That makes your dependency tree part of your attack surface, and it deserves the same scrutiny as the code you write yourself.


Six Controls for Defending Your Dependency Tree

A smart-contract audit does nothing against this class of attack. There was no contract bug to find. The failure was an account, a pipeline, and a dependency. Six controls move the needle here.

  1. Pin and verify, do not float. Use a lockfile and pin exact versions. Turn on npm audit signatures and npm provenance / package attestations so an unsigned or unexpected build fails your install instead of silently landing. A range like ^1.20.0 would have pulled 1.20.21 the moment it published.

  2. Never derive keys inside a dependency you cannot see. Treat fromMnemonic and fromHex as a trust boundary. Where the stakes justify it, derive and sign inside a hardware wallet or an isolated signer process, so a poisoned SDK never touches the plaintext seed. The Injective payload only worked because the secret flowed through library code in the same process.

  3. Monitor egress from build and runtime, not just the chain. The exfil here was an outbound POST with a base64 X-Request-Id header and an empty body. Anomalous, if anyone was watching application egress. Allowlist the hosts your app is supposed to talk to and alert on the rest. This is the layer our Tripwire monitoring exists for: catching the network behavior an audit can never see, because it does not exist until the code runs.

  4. Put a human pause on wallet-adjacent releases. Full OIDC auto-publish is right for most packages. For the handful that touch keys, require a second maintainer's approval and a diff review before publish. The six-minute fan-out was possible only because nothing sat between the version bump and the world.

  5. Lock the identity. This started with a maintainer's account. Public reporting has not established how that account was taken, and it does not say whether 2FA was in place, so the fix is a class of controls rather than a single missing one: hardware-backed 2FA on every maintainer and CI identity, session-token hygiene, and branch protection that a single compromised committer cannot bypass alone. Ledger's 2023 attack has a known way in, a phished npm account; Injective's does not, beyond the fact that a trusted account pushed the commits. Same category either way: both are identity failures, and hardware-backed maintainer auth blunts them both.

  6. Assume the deprecated version still ships. Deprecation does not remove a package. A pin to 1.20.21 in a lockfile will happily reinstall it, npm's warning notwithstanding, so scan your lockfiles for known-bad exact versions and block them at CI.


Frequently Asked Questions

Which Injective npm packages were compromised? Version 1.20.21 of @injectivelabs/sdk-ts plus 17 dependent @injectivelabs packages that pin it, including the wallet-* family (wallet-base, wallet-core, wallet-strategy, wallet-private-key, and others). All 18 were published within about six minutes. The clean, safe version is 1.20.23.

Did anyone lose crypto in the Injective npm attack? Injective's CEO says no network funds are at risk, no theft has been confirmed on-chain, and the short live window supports that: the package was installable from npm for only about 17 minutes. But any seed phrase or private key that passed through an affected build during that window should be treated as compromised. Rotate those keys and move the funds regardless.

Would a smart-contract audit have caught this? No. There was no contract vulnerability. This was a compromised developer account pushing a backdoor into an npm package. The defenses that matter here are dependency pinning and provenance, identity security on maintainer accounts, and runtime egress monitoring, not on-chain code review.

How was the stolen data exfiltrated without being noticed? The backdoor base64-encoded the keys into an X-Request-Id HTTP header and sent them with a Content-Type of application/grpc-web+proto, shaping the traffic to look like a normal Injective gRPC-web request. It also batched derivations and flushed on a two-second timer, and hid the destination domain as a char-code array to evade plaintext scanning.

Is the malicious version still available? The bad version was deprecated on npm, not removed, and the malicious artifacts remained on the project's GitHub releases page after deprecation. A build that pins the exact bad version can still install it, so scan and block it in CI.


Sources / References

Dmitry Serdyuk
Dmitry Serdyuk

Co-Founder & CDO

Full-Stack Operator | Building across security, AI, and digital infrastructure.