---
title: "Backdoored node-ipc npm releases steal developer credentials through DNS queries"
description: "An analysis of backdoored node-ipc npm releases that add an obfuscated credential collection and DNS exfiltration payload to the CommonJS entrypoint."
author: "Nick Frichette"
date: 2026-05-14
category: "emerging threats and vulnerabilities"
tags: ["software supply-chain security", "threat research", "application security"]
url: "https://securitylabs.datadoghq.com/articles/node-ipc-npm-malware-analysis/"
---

## Key points and observations

- On May 14, 2026, three `node-ipc` releases were published to npm with a backdoored CommonJS entrypoint. Releases: `9.1.6`, `9.2.3`, and `12.0.1`.
- All three versions contained the same malicious `node-ipc.cjs` file.
- The malicious block is a credential collection and exfiltration payload. It collects environment variables, host information, `/etc/hosts`, and a broad set of developer, cloud, package manager, source control, Kubernetes, database, and SSH credential files.
- The payload is not triggered by an npm life cycle script. It runs when the package's CommonJS entrypoint is loaded, such as through `require("node-ipc")`.
- The exfiltration path is DNS-based. The payload prepares a gzip-compressed tar archive, decodes `sh.azurestaticprovider.net:443` as its remote DNS endpoint, and builds DNS TXT query names under the decoded suffix `bt.node.js`.

## What the malware does

At a high level, the backdoor runs when the CommonJS entrypoint is loaded, forks a detached child process, collects host and credential files into an archive, and attempts to exfiltrate that archive through DNS TXT queries.

![Attack flow](https://securitylabs.dd-static.net/img/node-ipc-npm-malware-analysis/node-ipc-attack-flow.png?auto=format)
*Attack flow (click to enlarge).*

The payload runs from the top level of `node-ipc.cjs`. If the current process has not already set the environment variable `__ntw=1`, the code attempts to fork the current module in a detached child process, passes `__ntw=1` to that child, removes `NODE_OPTIONS`, ignores standard I/O, and returns control to the parent. The detached fork gives the parent process a normal package load path while the collection logic continues in the background.

The payload then builds a collection archive. It manually creates tar entries, gzip-compresses the archive, writes a temporary `*.tar.gz` file under a process-specific temporary directory named like `nt-<pid>`, calls the exfiltration routine, and then deletes the temporary archive in a `finally` block.

The archive contains:

- `uname.txt`: Output from `uname -a`, when available.
- `etc/hosts`: A copy of `/etc/hosts`, when readable.
- `envs.txt`: Sorted environment variables from the process.
- Matched credential files: Files from a decoded platform-specific target list, capped at roughly 4 MiB each.
- `fixtures/_paths.txt`: A newline-separated manifest of original file paths that were included in the archive.

Collected files are renamed before being placed in the archive under names matching `fixtures/f_<16 hex>_<basename>`. The malware hashes each original path with SHA-256, uses the first 16 hex characters, and combines that value with a sanitized basename. This naming scheme keeps the archive's filenames compact while preserving enough information to map files back to their original paths through the manifest.

The decoded target list covers local developer secrets, package manager tokens, source control credentials, cloud provider credentials, infrastructure configuration, and desktop app token stores. Examples include:

| Category | Examples from the decoded target lists |
| :---- | :---- |
| Cloud credentials | `~/.aws/credentials`, `~/.azure/accessTokens.json`, `~/.config/gcloud/application_default_credentials.json`, `~/.oci/config` |
| Package and source control credentials | `.npmrc`, `~/.npmrc`, `~/.git-credentials`, `~/.config/gh/hosts.yml`, `.git/config` |
| SSH and Kubernetes material | `~/.ssh/id_rsa`, `~/.ssh/id_ed25519`, `~/.kube/config`, `/var/run/secrets/kubernetes.io/serviceaccount/token` |
| Application and infrastructure secrets | `.env`, `.env.production`, `config/database.yml`, `terraform.tfvars`, `wp-config.php` |
| Local desktop credential stores | macOS Keychain databases, Firefox key databases, Linux keyrings, KWallet files, and Microsoft Teams LevelDB files |

The file discovery logic handles exact paths, home directory expansion, globbed basenames, directory wildcards, and recursive `**/` searches from the current working directory. During recursive searches, it skips directories named `node_modules` and `.git`. Those exclusions still leave many project directories in scope, especially when secrets are stored in files such as `.env`, GitHub workflow files, Terraform variable files, and application configuration.

The exfiltration logic is DNS-based. The malware decodes `sh.azurestaticprovider.net:443` as a remote DNS endpoint. It first resolves the hostname with `dns.Resolver` using public resolvers including `8.8.8.8` and `1.1.1.1`. The upload path then sets the resolver to the resolved endpoint and sends archive metadata and body chunks as TXT lookup requests using generated names under `bt.node.js`.

## C2 and exfiltration over DNS

The command and control (C2) channel uses DNS, not HTTP. The decoded endpoint, `sh.azurestaticprovider.net:443`, is used as a DNS resolver endpoint. The `:443` suffix is important. In this payload, port 443 is DNS over a nonstandard port, not HTTPS.

The payload resolves `sh.azurestaticprovider.net` through public resolvers, then configures a new `dns.promises.Resolver()` instance to use the resolved IP address on port 443. On May 14, 2026, lookups from our environment resolved `sh.azurestaticprovider.net` to `37.16.75.69` with a 300-second TTL. After configuring the custom resolver, the payload calls `resolveTxt(...)` with generated query names. It does not need useful DNS answers because the stolen data is encoded into the query name itself; lookup errors are caught and ignored.

The query names use three prefixes:

| Prefix | Purpose |
| :---- | :---- |
| `xh` | Header chunk |
| `xd` | Data chunk |
| `xf` | Footer chunk |

The general query shapes are:

```text
xh.{machineHex}.{sessionId}.{sig}.{chunkIndex}.{hexChunk}.bt.node.js
xd.{machineHex}.{sessionId}.{sig}.{chunkIndex}.{hexChunk}.bt.node.js
xf.{machineHex}.{sessionId}.{sig}.0.{hexChunk}[.{hexChunk}...].bt.node.js
```

In those names, `machineHex` is derived from host fingerprinting, `sessionId` is generated from 5 random bytes and encoded as 10 hex characters, `sig` is a 12-character truncated HMAC-SHA256 value, `chunkIndex` is a decimal chunk number, and `hexChunk` is DNS-safe encoded payload material.

The header is JSON, hex-encoded, and split into DNS labels. It includes metadata such as:

```json
{
  "v": 1,
  "machineHex": "...",
  "cloud": "none",
  "archivePath": "/tmp/nt-<pid>/<machineHex>.tar.gz",
  "gzipBytes": 12345,
  "hdrChunks": 2,
  "datChunks": 400,
  "hostLabel": "hostname"
}
```

The archive data path is more layered. The payload builds the tar archive, gzip-compresses it, converts the gzip bytes to base64 text, XORs that text with a SHA-256-derived keystream seeded from the hardcoded key and `machineHex`, re-encodes the XORed bytes as base64, substitutes the base64 alphabet with a keyed shuffled alphabet, slices the transformed string into 31-character chunks, and hex-encodes each chunk before placing it into DNS labels. In the sample we analyzed, header chunks are up to 63 characters, data chunks are 31 characters before hex encoding, TXT lookups are sent in batches of up to 160, and the resolver timeout is about 8 seconds.

## Indicators of compromise

The following indicators come from the tarballs and static deobfuscation:

| Type | Context | Indicator |
| :---- | :---- | :---- |
| Package | Affected version | `node-ipc@9.1.6` |
| Package | Affected version | `node-ipc@9.2.3` |
| Package | Affected version | `node-ipc@12.0.1` |
| Tarball SHA-256 | `node-ipc@9.1.6` | `449e4265979b5fdb2d3446c021af437e815debd66de7da2fe54f1ad93cbcc75e` |
| Tarball SHA-256 | `node-ipc@9.2.3` | `c2f4dc64aec4631540a568e88932b61daebbfb7e8281b812fa01b7215f9be9ea` |
| Tarball SHA-256 | `node-ipc@12.0.1` | `78a82d93b4f580835f5823b85a3d9ee1f03a15ee6f0e01b4eac86252a7002981` |
| File SHA-256 | `node-ipc.cjs` | `96097e0612d9575cb133021017fb1a5c68a03b60f9f3d24ebdc0e628d9034144` |
| DNS host | Exfiltration endpoint host | `sh.azurestaticprovider.net` |
| DNS A record | Observed on May 14, 2026 | `37.16.75.69` |
| DNS port | Exfiltration endpoint port | `443` |
| DNS query suffix | TXT lookup suffix | `bt.node.js` |
| Environment marker | Child process environment | `__ntw=1` |
| Export marker | Static code marker | `__ntRun` |
| Temporary path pattern | System temporary directory | `<tmpdir>/nt-<pid>/` |
| Archive names | Under the temporary `nt-<pid>` directory | `*.tar.gz` |
| Archive entries | Archive contents | `uname.txt`, `etc/hosts`, `envs.txt` |
| Archive entry pattern | Collected files | `fixtures/f_<16 hex>_<basename>` |
| Archive manifest | Collected path manifest | `fixtures/_paths.txt` |
| Embedded key material | Payload configuration | `qZ8pL3vNxR9wKmTyHbVcFgDsJaEoUi` |
| Custom encoding alphabet | Payload decoder | `0123456789GHJKMP` |

## Detection and response

Defenders should start with dependency inventory. On a per-project basis, search package manifests and lock files for `node-ipc` entries, then confirm whether the resolved versions are `9.1.6`, `9.2.3`, or `12.0.1`:

```shell
grep -R 'node-ipc@\|"node-ipc"' package.json package-lock.json yarn.lock pnpm-lock.yaml npm-shrinkwrap.json 2>/dev/null
```

To search your GitHub repository or organization, use the following query:

```text
/node-ipc@|"node-ipc"/ path:/(^|\/)(package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|npm-shrinkwrap\.json)$/
```

If installed dependencies are available for inspection, look for the backdoored CommonJS file hash, the literal export marker, or encoded configuration strings in `node_modules/node-ipc/node-ipc.cjs`:

```shell
shasum -a 256 node_modules/node-ipc/node-ipc.cjs
grep -F -e '__ntRun' \
  -e '3786M216G75727563747164796360727P66796465627M2M65647G34343339' \
  -e '2647M2M6P64656M2G637H' \
  node_modules/node-ipc/node-ipc.cjs
```

In endpoint, CI, and network telemetry data, useful signals include:

- A Node.js process loading `node-ipc` and then spawning a detached child process with `__ntw=1`.
- Node.js processes reading many credential files shortly after package load.
- Temporary directories matching `nt-<pid>` under the system temporary directory, especially containing short-lived `*.tar.gz` files.
- DNS queries for `sh.azurestaticprovider.net` while resolving the configured endpoint.
- Unexpected DNS resolution attempts from build systems or developer workstations directly to `8.8.8.8` or `1.1.1.1` shortly after loading `node-ipc`.
- DNS TXT lookups using query names under `bt.node.js` from the same process or host that loaded `node-ipc`.

For containment, start with DNS sinkholing and egress controls. Sinkhole or block `sh.azurestaticprovider.net` so the payload cannot resolve its custom resolver endpoint through managed DNS. Because the payload attempts to resolve that name through public resolvers, pair the sinkhole with controls that block direct DNS to unapproved resolvers such as `8.8.8.8` and `1.1.1.1`. Teams should also block DNS-like traffic to `37.16.75.69:443`, where port 443 is used for DNS rather than HTTPS. Where DNS tooling supports full query-name matching, sinkhole or alert on `*.bt.node.js`, especially query names beginning with `xh`, `xd`, or `xf`.

Hosts that installed and loaded one of these versions should be treated as potential credential exposure events. Response should include removing the affected package version, preserving relevant host and CI telemetry data, rotating credentials that may have been present in the decoded target paths, and reviewing package publishing tokens, GitHub tokens, SSH keys, cloud credentials, Kubernetes credentials, and CI secrets.

## How Datadog can help

[Datadog Code Security](https://docs.datadoghq.com/security/code_security/) customers can use [Software Composition Analysis (SCA)](https://docs.datadoghq.com/security/code_security/software_composition_analysis/) to identify repositories and running services that reference affected `node-ipc` versions. Use the following query in the [Library Inventory](https://app.datadoghq.com/security/code-security/inventory/libraries) to scope exposure:

```text
library_name:node-ipc status:Open library_version:(9.1.6 OR 9.2.3 OR 12.0.1)
```

You can also open a prefilled [Library Inventory query for the affected `node-ipc` versions](https://app.datadoghq.com/security/code-security/inventory/libraries?query=library_name%3Anode-ipc%20status%3AOpen%20library_version%3A%289.1.6%20OR%209.2.3%20OR%2012.0.1%29). Static SCA helps find declared dependencies in repositories, while Runtime SCA helps identify libraries loaded by monitored services. That distinction matters here because the payload runs when the CommonJS entrypoint is loaded, not during installation. When the query returns results, prioritize services where the library was loaded at runtime, identify the owning repositories and services, and use that scope to guide credential rotation and host review.

## Recommendations for dependency consumers and maintainers

Dependency consumers should pin known-good versions, review newly published releases before adopting them, and consider dependency cooldown settings where package managers or update bots support them. A short delay is useful for newly published patch versions of widely used packages because it gives maintainers and the security community time to identify suspicious releases before automation pulls them into builds.

Consumers should also monitor both install-time and runtime package behavior. This incident did not rely on an npm life cycle hook. A check that only blocks `preinstall`, `install`, or `postinstall` scripts would miss the execution path for CommonJS consumers.

Maintainers should use trusted publishing, require multifactor authentication for registry accounts, restrict long-lived publish tokens, and monitor registry metadata for unexpected direct publishes. For packages that ship both ES module and CommonJS entrypoints, maintainers should treat generated bundles as release artifacts that need review. A small source diff can still hide a large malicious bundle diff.
