Supply Chain Attacks

node-ipc Compromised Again (14 May 2026): An 80 KB Credential Stealer in a 10M-Download Library

On 14 May 2026, three malicious node-ipc versions (9.1.6, 9.2.3, 12.0.1) shipped an 80 KB credential-stealing IIFE appended after module.exports in the CJS bundle — no install scripts, harvesting 90+ secret categories from a library with 10M+ weekly downloads.

Safeguard Research Team
Threat Intelligence
9 min read

In 2022, node-ipc became the textbook example of protestware: its maintainer shipped a payload that wiped files on machines geolocated to Russia and Belarus. In May 2026 the same package is back in the headlines, but the motive has inverted. On 14 May 2026, three malicious versions of node-ipc — 9.1.6, 9.2.3, and 12.0.1 — were published to npm carrying an 80 KB obfuscated credential-stealing payload. This is not protestware and, per StepSecurity, not the original author; it is a financially-motivated actor harvesting secrets across more than 90 categories from a library that pulls over 10 million downloads a week.

The technique is the part worth your attention. The payload carried no install script. It was an Immediately Invoked Function Expression appended to the very end of the CommonJS bundle, after the final module.exports, so it executed whenever any dependent required the library — and it was invisible to the entire class of tooling that scans for malicious postinstall/preinstall hooks. This analysis is grounded in StepSecurity's reconstruction and the published IOCs. Where a detail is reported versus inferred, we flag it. The headline lesson: "we block packages with install scripts" is not a defense against a payload that lives in the runtime require path.

TL;DR

  • On 14 May 2026, three malicious node-ipc versions were published to npm: 9.1.6, 9.2.3, and 12.0.1, from the publisher account atiertant.
  • node-ipc provides inter-process communication for Node.js and has 10M+ weekly downloads, sitting deep in many dependency trees.
  • The payload was an 80 KB obfuscated IIFE appended after module.exports in node-ipc.cjs. No install scripts were used, so lifecycle-hook scanners did not see it; it ran on require.
  • It harvested credentials across 90+ categories: AWS, Azure, GCP, SSH keys, Kubernetes tokens, GitHub configs, database passwords, and more.
  • StepSecurity's analysis distinguishes this from the 2022 peacenotwar protestware (versions 10.1.1-10.1.2): a different actor with a purely financial credential-theft motive.
  • IOCs: C2 sh.azurestaticprovider[.]net / 37.16.75.69; payload SHA-256 96097e0612d9575cb133021017fb1a5c68a03b60f9f3d24ebdc0e628d9034144.
  • Monday-morning actions: pin node-ipc away from 9.1.6/9.2.3/12.0.1, rebuild from clean lockfiles, block the C2 IOCs, and rotate any secret reachable from a host or runner that resolved a bad version.

What happened

node-ipc is a long-standing Node.js library for inter-process and inter-socket communication. It is widely depended upon, both directly and transitively, with StepSecurity citing over 10 million weekly downloads. Originally authored by Brandon Nozaki Miller, it carries history: in March 2022 the maintainer introduced the peacenotwar module and, in versions 10.1.1-10.1.2, destructive behavior targeting systems geolocated to Russia and Belarus.

The May 2026 event is a separate incident. According to StepSecurity, on 14 May 2026 three malicious versions — 9.1.6, 9.2.3, and 12.0.1 — were published from the account atiertant (a.tiertant@atlantis-software.net). StepSecurity is explicit that this represents "a different actor with a purely financial credential-theft motive" compared with the 2022 protestware, and that the original author is not implicated. The publishing of three different version lines (a 9.x patch, another 9.x, and a 12.x) in one event is consistent with an attacker who has obtained publish rights and wants to maximize the chance that whatever range a consumer's lockfile allows will resolve to a malicious build.

Whether the atiertant account was newly created, compromised, or added as a collaborator is not established in the public reporting. What is clear is that the actor had the rights to publish under the node-ipc name.

How the attack worked

The IIFE-after-exports trick

The defining technical detail is where the payload lived. Instead of a postinstall script — the most-scanned, most-blocked malicious-publish vector — the actor appended an Immediately Invoked Function Expression to the end of the package's compiled CommonJS bundle, node-ipc.cjs, after the final module.exports line.

// Illustrative shape — NOT functional exploit code.
// ... legitimate node-ipc implementation ...
module.exports = IPC;          // the real, expected export

// 80 KB of obfuscated payload appended AFTER exports, runs on require():
(function () {
  /* deobfuscate; sweep 90+ credential locations; exfiltrate to C2 */
})();

Because the IIFE is part of the module body, it executes the moment the file is loaded — that is, whenever any code in the dependency tree does require("node-ipc"). No npm install lifecycle hook is involved. This matters for two reasons. First, the large population of defenses built specifically around install-script auditing (--ignore-scripts, postinstall allow-lists, sandboxed lifecycle execution) provides zero coverage here. Second, the payload runs in the application's own process at runtime, in production and CI alike, not just on the developer's machine at install time.

Obfuscation and credential harvesting

The appended payload is roughly 80 KB and obfuscated. Once deobfuscated and executed, StepSecurity reports it sweeps more than 90 categories of credentials, including AWS, Azure, and GCP credentials, SSH keys, Kubernetes tokens, GitHub configuration, and database passwords. This broad-spectrum sweep is the same pattern seen across the May 2026 wave: read every well-known credential location on disk and in the environment, then exfiltrate.

// Illustrative reconstruction of the harvesting shape — NOT functional code.
const probes = [
  process.env,                       // env vars, API keys
  "~/.aws/credentials",
  "~/.azure", "~/.config/gcloud",
  "~/.ssh/id_*", "~/.kube/config",
  "~/.gitconfig", "~/.npmrc",
  // ...90+ locations total
];
// collect -> obfuscated bundle -> POST to C2 over HTTPS

Exfiltration

Collected secrets were exfiltrated to the command-and-control infrastructure: domain sh.azurestaticprovider[.]net (a lookalike trading on Azure's name) resolving to 37.16.75.69. The malicious bundle has SHA-256 96097e0612d9575cb133021017fb1a5c68a03b60f9f3d24ebdc0e628d9034144.

What detection looks like

Pinned malicious versions in lockfiles. The most concrete signal is the presence of node-ipc@9.1.6, 9.2.3, or 12.0.1 anywhere in your resolved dependency graph.

# Surface any node-ipc resolution and flag the known-bad versions.
jq -r '
  .packages | to_entries[]
  | select(.key | endswith("node_modules/node-ipc"))
  | .key + "@" + (.value.version // "?")
' package-lock.json | grep -E '@(9\.1\.6|9\.2\.3|12\.0\.1)$' && echo "MALICIOUS node-ipc PINNED"

Bundle-tail anomaly. Because the payload sits after module.exports, a high-signal check is to look for executable code (especially a large obfuscated IIFE) following the final export in node-ipc.cjs. Compare the installed node-ipc.cjs against a known-good version; a sudden ~80 KB tail is the tell. The bundle SHA-256 above is a direct match check.

# Compare the installed bundle hash against the known-bad payload hash.
sha256sum node_modules/node-ipc/node-ipc.cjs 2>/dev/null \
  | grep -qi '96097e0612d9575cb133021017fb1a5c68a03b60f9f3d24ebdc0e628d9034144' \
  && echo "MATCH: known-bad node-ipc bundle"

Network IOCs. Alert on and block egress to sh.azurestaticprovider[.]net and 37.16.75.69 from application servers, CI runners, and developer machines. Any host that contacted it since 14 May should be treated as compromised.

Runtime behavior. Because the payload runs at require time, the credential sweep happens in your application/CI process. Outbound connections to the C2 from a Node process that has no business making them, or bursts of reads against ~/.aws, ~/.ssh, and ~/.kube shortly after process start, are behavioral indicators.

What to do Monday morning

  1. Pin away from the bad versions and rebuild clean. Constrain node-ipc to a known-good version, regenerate the lockfile deterministically, and rebuild. Do not trust a loose npm install to avoid the malicious range.
  2. Rotate every secret reachable from an affected host or runner. The payload runs at runtime in your process and sweeps 90+ credential categories. If any environment resolved 9.1.6/9.2.3/12.0.1 and required the library, treat AWS/Azure/GCP keys, SSH keys, Kubernetes tokens, GitHub tokens, and DB passwords on that host as burned.
  3. Block the C2 IOCs at egress: sh.azurestaticprovider[.]net and 37.16.75.69.
  4. Stop relying on install-script scanning alone. This payload proves that vector-blind. Add bundle-integrity/provenance verification so a tampered runtime file is caught even with no lifecycle hook.
  5. Hunt the bundle hash fleet-wide. Use the SHA-256 to sweep installed node_modules across build caches, container images, and developer machines, not just your primary repo.
  6. Check container images and artifacts built since 14 May. A malicious node-ipc baked into an image will keep exfiltrating in production until rebuilt; rebuild and redeploy affected images.

Why this keeps happening

The ecosystem spent years hardening the install-script surface, and that work was worthwhile — install hooks were the dominant malicious-publish vector. But hardening one vector teaches attackers to use another. A payload in the runtime require path is strictly more powerful than one in a postinstall hook: it runs in production, not just at install, and it sidesteps the entire toolchain built around lifecycle-script auditing. The node-ipc 2026 compromise is a clean demonstration that "no install scripts" is not "no malicious code."

The second structural fact is that a 10-million-download library is a force multiplier. Most consumers never read the bundle of a transitive dependency; they trust the name and the download count. A version range in a single direct dependency's manifest can silently resolve a deep-transitive node-ipc to a malicious build, and the integrity gap — between "tarball published" and "tarball verified to match expected provenance" — is exactly where this payload lived.

The structural fix

The honest framing is faster detection and a tighter blast radius. A malicious-package feed sourced from multiple research vendors plus OSV surfaces a known-bad node-ipc version against your inventory in minutes, which is the difference between rotating before the attacker pivots and finding out from a breach notification. SLSA provenance and signature verification at the package-proxy layer reject a tarball whose build origin or signature does not match the project's expected publishing identity — the control that catches a tampered runtime bundle even when there is no install hook to scan. A maintained SBOM plus reachability analysis tells you which services actually require node-ipc on a reachable path, so credential rotation and redeploys target the genuinely exposed systems first. None of this prevents the upstream publish; it compresses dwell time and contains the radius.

What we know we don't know

  • How the atiertant account obtained publish rights (new account, compromise, or collaborator addition) is not public.
  • The total number of organizations that resolved a malicious version during the live window is not reported.
  • How long the three malicious versions remained installable before yanking, and the precise unpublish timeline, are not detailed in the sources reviewed.
  • Whether this actor overlaps with the concurrent TeamPCP "Mini Shai-Hulud" campaign is not established; the motive (purely financial) and tradecraft (runtime IIFE, no install script) differ from the CI-native worm pattern.

References

Internal reading:

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.