Worked example · 2026-07
Injective Labs SDK — a commit from a trusted maintainer account rides the OIDC auto-publish pipeline into 18 npm packages that exfiltrate seed phrases at key-derivation time — Injective Labs / npm (chain-agnostic) — 2026-07-08
Summary
@injectivelabs/sdk-ts is Injective Labs' TypeScript SDK, drawing about 50,000 weekly downloads and used to build wallets, trading bots, decentralised exchanges, DeFi applications, and payment tools on Injective. Its GitHub repository publishes to npm through an OIDC trusted-publisher pipeline: a version bump on the repository triggers an automatic, credential-less publish whose provenance is cryptographically attested back to the repository.
On 2026-07-08 at 20:24 UTC, a commit authored under a maintainer account with an established contribution history added src/utils/key-derivation-telemetry.ts — 79 lines presenting themselves as telemetry. A version bump at 20:54 triggered the pipeline, and between 20:59 and 21:00 all 18 @injectivelabs-scoped packages were published at v1.20.21: sdk-ts itself plus networks, wallet-base, wallet-core, wallet-strategy, wallet-cosmos, wallet-cosmostation, wallet-evm, wallet-ledger, wallet-trezor, wallet-magic, wallet-private-key, wallet-turnkey, wallet-wallet-connect, and four others carrying transitive risk.
The payload did not run on install. It hooked PrivateKey.fromMnemonic() and PrivateKey.fromHex() — the functions a developer calls to derive or import a key — so it fired only when real key material passed through, which is what let it sit inside a legitimate-looking telemetry module. On trigger it captured the full mnemonic and private key, base64-encoded them, and batched them in memory on 2-second windows. Exfiltration went out as a POST with Content-Type: application/grpc-web+proto and an empty body, with the secrets carried in the X-Request-Id header — traffic shaped to look like ordinary gRPC-Web chatter. The destination hostname (testnet.archival.chain.grpc-web.injective.network, mimicking Injective's real gRPC-Web infrastructure) never appeared as plaintext in source: it was stored as a JavaScript character-code array and reconstructed at runtime. Errors were swallowed silently so nothing surfaced to the developer.
A revert commit removed the payload at 21:16, and a clean v1.20.23 was published at 21:47–21:49 with v1.20.21 deprecated on the registry. Total exposure window was under an hour; the tainted version recorded 310 downloads. Injective reported the compromise resolved with no user impact. Anyone who installed v1.20.21 in the window should treat every key and mnemonic that touched the package as compromised, rotate them, and audit transitive dependencies.
Timeline (UTC)
| When | Event | OAK ref |
|---|---|---|
| (standing) | Repository publishes to npm via an OIDC trusted-publisher pipeline: a version bump auto-publishes with attested provenance and no npm token in the loop | (standing T15.002 surface) |
| 2026-07-08 20:24 | Commit authored under a trusted maintainer account adds src/utils/key-derivation-telemetry.ts (79 lines) |
T15.002 injection |
| 2026-07-08 20:54 | Version bump triggers the CI/CD publish pipeline | T15.002 propagation |
| 2026-07-08 20:59–21:00 | All 18 @injectivelabs packages publish at v1.20.21, provenance-attested to Injective's repository |
T15.002 publication |
| 2026-07-08 (window) | Payload fires only on PrivateKey.fromMnemonic() / PrivateKey.fromHex(); captures mnemonic + private key, base64, 2-second in-memory batching; exfiltrates via empty-body POST with secrets in the X-Request-Id header to a runtime-reconstructed lookalike hostname |
T11.009 |
| 2026-07-08 21:16 | Revert commit removes the payload | (containment) |
| 2026-07-08 21:47–21:49 | Clean v1.20.23 published; v1.20.21 deprecated on npm | (remediation) |
| post-event | 310 recorded downloads of v1.20.21 against ~50,000 weekly; Injective reports resolution within the hour and no user impact | (operator response) |
What defenders observed
- Pre-event (the repository was the trust root, and one account could move it). The pipeline's security model was "commits in this repository are trustworthy, therefore builds from this repository may publish automatically". A single compromised maintainer account satisfied that model completely. The control is to require something an individual account holder cannot unilaterally provide on the paths that matter: mandatory second-maintainer review for commits touching key-handling code, protected paths with signed approval, or a publish delay that puts a human diff review between merge and registry. Five minutes elapsed between the version bump and 18 published packages (M40).
- Pre-event (auto-publish multiplied one commit into 18 packages). Fan-out is a design property here, not an accident: the packages publish together, so the blast radius of any single injection is the whole scope, including the
wallet-ledgerandwallet-trezorpackages whose users had specifically chosen hardware isolation. Release fan-out should be an explicit part of a project's threat model — the question is how many artefacts a single merge can ship, and whether that number is acceptable without review. - At-event (the payload triggered on key derivation, not on install). Install-time scanning — the assumption behind much npm supply-chain tooling — would have found nothing. The malicious code only executed when
PrivateKey.fromMnemonic()orPrivateKey.fromHex()was called with real key material. Detection has to look at what a package does with secrets it is handed, not only at what it does at install (T11.009). - At-event (exfiltration shaped to look like the SDK's own traffic). Secrets travelled in an
X-Request-Idheader on an empty-body POST typedapplication/grpc-web+proto, to a hostname that mimicked Injective's real gRPC-Web infrastructure and was reconstructed at runtime from a character-code array so it never appeared in source. Every choice here defeats a specific control: header-channel smuggling defeats body inspection, the lookalike hostname defeats domain-reputation checks, runtime reconstruction defeats static string scanning, and silent error handling defeats developer-visible failure. Egress allowlisting from build and bot environments is the control that survives all of them. - Detection and response (fast, and largely luck-independent of the consumer). The 50-minute window and the revert-then-clean-release sequence bounded this well, and Injective's response is the reason the incident reads as a near-miss. But the 310 downloads are not recoverable: exfiltration was silent, so no affected developer received a signal. Rotate-on-disclosure is the only sound posture for anyone who installed v1.20.21 — treat the keys as gone regardless of whether a breach is visible (M22).
- Consumer-side (a floating dependency inherits its publisher's worst hour). Downstream projects that resolve
@injectivelabs/*loosely would have picked up v1.20.21 automatically during the window. Pinning with a reviewed diff on version bumps is what converts a publisher compromise from an automatic exposure into a decision the consumer gets to make (M40).
Public references
[stepsecurityinjective2026]— StepSecurity, "Injective npm Supply Chain Attack: 18 Packages Backdoored to Steal Crypto Wallet Keys" (primary technical forensics: minute-level UTC timeline, all 18 package names,key-derivation-telemetry.tspayload location and size,PrivateKey.fromMnemonic()/fromHex()triggers, base64 + 2-second batching,X-Request-Idheader exfiltration withapplication/grpc-web+protocontent type, runtime-reconstructed hostname, OIDC trusted-publisher pipeline abuse without npm-token theft): https://www.stepsecurity.io/blog/injective-npm-supply-chain-attack-18-packages-backdoored-to-steal-crypto-wallet-keys[thehackernewsinjective2026]— The Hacker News, "Injective Labs GitHub Compromise Pushes Wallet-Key-Stealing npm Packages" (GitHub-repository compromise as the entry vector, commits from an established-history maintainer account, fake telemetry framing, 17 additional scoped packages): https://thehackernews.com/2026/07/injective-labs-github-compromise-pushes.html[bleepinginjective2026]— BleepingComputer, "Injective SDK on npm infected with cryptocurrency wallet stealer" (~50,000 weekly downloads, consumer cohort of wallets / bots / DEXes / payment tools, remediation to v1.20.23, key-rotation guidance): https://www.bleepingcomputer.com/news/security/injective-sdk-on-npm-infected-with-cryptocurrency-wallet-stealer/[socketinjective2026]— Socket, "Compromised Injective SDK npm Package Exfiltrates Wallet Keys" (independent payload analysis and behavioural breakdown): https://socket.dev/blog/compromised-injective-sdk-npm-package[cryptobriefinginjective2026]— Crypto Briefing, "Injective resolves npm package compromise in under an hour with zero user impact" (operator response, sub-hour resolution, 310 downloads of the malicious version, no-user-impact statement): https://cryptobriefing.com/injective-npm-package-security-fix/
Discussion
Injective belongs in OAK's T15.002 package-registry family alongside Ledger Connect Kit (2023-12), Solana web3.js (2024-12), and the Polymarket trader-tooling compromise (2026-01), and what it contributes to the class is the trusted-publisher inversion. The prior anchors are token-theft stories: someone stole a publish credential and used it. OIDC trusted publishing was designed to kill exactly that vector, and it did — there was no npm token here to steal. But it relocated the trust root to the repository, and the repository is guarded by maintainer accounts. The compromise therefore rode a pipeline that was, by design, credential-less, automatic, and provenance-attested. The malicious v1.20.21 carried a cryptographic attestation that it genuinely came from Injective's repository, and that attestation was true. Provenance answered the question it was built to answer and the answer was correct; the question was simply not the one that would have saved anyone.
This is worth stating carefully because the wrong lesson is available and attractive. Trusted publishing is better than long-lived npm tokens and projects should keep using it. The point is that provenance attests origin, not intent, and a supply chain whose only gate is origin will publish whatever a compromised maintainer account produces, at machine speed. The window from version bump to 18 live packages was about five minutes. The gate that closes this sits earlier — at the commit, on the paths that touch key material — and it must be a gate a single account cannot pass alone.
The payload's craft is the other durable contribution, and it is what makes the T11.009 mapping load-bearing rather than decorative. This was not a smash-and-grab postinstall script. It fired only when PrivateKey.fromMnemonic() or PrivateKey.fromHex() was handed real key material, which is both precise targeting and effective evasion: install-time scanners see an inert telemetry module, and the code is quiet on every machine that builds without touching a key. The exfiltration channel was chosen the same way — secrets in a header on an empty-body POST typed as gRPC-Web, to a hostname assembled at runtime that impersonated the SDK's own infrastructure, with errors swallowed. An analyst reading source would find no suspicious string; a network monitor without egress allowlisting would see the SDK talking to something that looked like Injective. Contributors documenting future registry compromises should record what triggers the payload as carefully as they record what it steals, because trigger design is where this cohort has been improving and it determines which detection layer has any chance of firing.
Finally, the 310 downloads are the reason this is filed as a near-miss rather than a non-event. Injective's response was genuinely fast and the outcome was probably close to the stated zero impact. But silent exfiltration means the absence of reported losses is not evidence of their absence, and the affected population — developers who happened to derive a key during a 50-minute window — has no way to self-identify. Rotate-on-disclosure exists precisely for this shape of uncertainty.