Socket’s threat feed has detected malicious activity in newly published versions of node-ipc, a long-running npm package previously associated with one of the most widely discussed supply chain incidents in the JavaScript ecosystem.
Socket’s AI scanner detected the newly published malicious versions within roughly three minutes of publication, classifying the activity as malware. Early analysis indicates that node-ipc@9.1.6, node-ipc@9.2.3, and node-ipc@12.0.1 contain obfuscated stealer/backdoor behavior. The malware appears to fingerprint the host environment, enumerate and read local files, compress and chunk collected data, wrap the payload in a cryptographic envelope, and attempt exfiltration through a network endpoint selected via DNS/address logic.
Socket’s incident response scan also noted historical malicious versions tied to the original 2022 node-ipc compromise. Versions 10.1.1 and 10.1.2 were associated with geo-targeted destructive malware that checked whether a system was located in Russia or Belarus before recursively overwriting files. Versions 11.0.0 and 11.1.0 included the peacenotwar dependency, which was previously linked to unauthorized file-writing behavior.
The latest incident appears to involve a suspicious republishing or reintroduction of malicious code into versions of a known package, rather than a typosquatting attempt. Socket classified all seven reviewed versions as malicious and recommends blocking them.
This is a developing story. Socket’s Threat Research team is continuing to analyze the package contents, confirm the full scope of the compromise, and extract indicators of compromise. Developers should avoid installing the affected versions and audit any recent installs of node-ipc, especially versions 9.1.6, 9.2.3, and 12.0.1.
node-ipc is an npm package for inter-process communication in Node.js applications. The compromised package metadata routes CommonJS and ESM consumers to different entrypoints:
The malicious code is only present in node-ipc.cjs. It is appended as a single obfuscated IIFE at the end of the bundled CommonJS file. The clean ESM wrapper, node-ipc.js, imports the package implementation from local source files and does not contain the appended payload.
The package tarballs also carry a forensic artifact: every file in the reviewed tarballs is timestamped Oct 26 1985. That timestamp appears across the analyzed version artifacts and should be treated as a package-level indicator when investigating cached tarballs or registry mirrors.
Maintainer Compromise
node-ipc has twelve listed npm maintainers. One of them, atiertant, has publish rights but has been dormant on this package for years; recent releases have been published by other maintainers. This account was used to publish the three malicious releases.
Independent researcher Ian Ahl (@TekDefense, CTO at Permiso) was first to publicly identify the likely vector: takeover of a dormant maintainer account via an expired email domain:
Assuming the npm account recovery email for atiertant was indeed hosted on atlantis-software[.]net, the new domain owner was then able to trigger a standard npm password reset, receive the reset email at a mailbox under their control, and gain publish rights without ever compromising any of the maintainer's own infrastructure.
When require("node-ipc") resolves to node-ipc.cjs, the appended IIFE executes during module load. The payload does not use a postinstall script. It relies on runtime loading of the CommonJS bundle.
The payload exposes its runner function as __ntRun on the module exports in the normal path:
// Analyst note: simplified from the decoded payload.
runner.__ntRun = runner;
if (module && module.exports) {
if (hashGateMatched) {
module.exports = runner;
} else {
module.exports.__ntRun = runner;
}
}
The __ntRun export is not just metadata. Any code that calls require("node-ipc").__ntRun() can trigger the runner again in that process. That creates an additional activation path through tests, build scripts, or other packages that inspect and call exported properties. Because the runner normally forks a detached child with __ntw=1, a downstream package that calls this export can cause a new collection and exfiltration attempt outside the original module-load event. The primary activation still happens automatically through setImmediate() after the CommonJS module loads.
The payload uses an environment variable named __ntw to distinguish the child execution path:
If the fork succeeds, the parent process returns and the credential collection runs in the detached child. If the fork fails, or if __ntw=1 is already present, the payload runs collection in the current process.
Hash Gate Changes Export Behavior
The payload computes a special hash gate from the basename of the current module filename:
// Analyst note: simplified from the decoded payload.
const actual = sha256(path.basename(module.filename).toLowerCase()).toString("hex");
const expected = "bf9d8c0c3ed3ceaa831a13de27f1b1c7c7b7f01d2db4103bfdba4191940b0301";
const hashGateMatched = actual === expected;The reviewed artifacts confirm credential and configuration theft from developer machines and CI-like environments that load the CommonJS entrypoint
For the analyzed node-ipc.cjs file, the basename hash does not match the expected value, so the package API is augmented with __ntRun and the rest of the library remains usable. If the gate matches in another filename context, the code replaces module.exports with the runner function. The filename preimage for the expected hash was not recovered from the local samples, so this should be described as a confirmed behavior branch with an unknown target filename.
Host Reconnaissance and Environment Collection
The payload builds a host fingerprint using Node.js OS APIs:
It also runs uname -a with a timeout and falls back to the Node.js fingerprint if that command fails. The archive always includes:
uname.txt, containing the command output or fallback fingerprint
etc/hosts, if /etc/hosts can be read
envs.txt, containing sorted process.env entries as KEY=value
The environment collection is not selective. Any token, cloud credential, CI secret, registry token, or database secret present in the process environment is written into envs.txt.
Credential and Configuration Harvesting
The payload chooses between separate decoded target lists for macOS and Linux/default platforms. The lists are not identical. In the analyzed payload, the macOS list contains 113 patterns and the Linux/default list contains 127 patterns. This count comes from the single identical malicious CommonJS file shared by all three reviewed package artifacts, not from a version-specific payload difference.
The collection logic supports home-directory expansion, relative paths from process.cwd(), single-segment globs, one-level dir/*/name patterns, and recursive **/name patterns from the current working directory. Recursive walking skips directories named node_modules and .git. Files larger than 4 MiB are skipped.
The targets focus on developer and infrastructure secrets, including:
FileZilla, Remmina, OpenVPN, and related connection profiles
Microsoft Teams local storage and IndexedDB paths
Browser credential theft should be described precisely. The payload does not directly target Chrome Login Data, Chrome cookies, Chrome history, Safari history, or Safari cookies in the analyzed lists. It does target macOS Keychain databases, which can contain system and browser credentials, and it targets Firefox key*.db files on macOS, but it does not collect Firefox logins.json or cookies.sqlite in the decoded target list.
That file maps the harvested archive entries back to their original absolute paths, giving the threat actor context for each collected credential.
Archive Construction and Temporary File Behavior
The payload builds a POSIX ustar archive in memory, writes file headers and padded contents, then gzip-compresses the full tar buffer.
It does not keep the archive only in memory. The child writes a gzip archive to a temporary directory before exfiltration:
<os.tmpdir()>/nt-<pid>/<machineHex>.tar.gz
The payload attempts to delete this file with unlinkSync() after the exfiltration attempt finishes. If the process is interrupted, crashes, or is contained during execution, responders may find the temporary archive on disk.
DNS TXT Exfiltration Instead of HTTP
The payload does not use http, https, or tls.connect() for exfiltration. It uses DNS TXT queries.
The decoded bootstrap resolver is:
sh[.]azurestaticprovider[.]net:443
The domain sh.azurestaticprovider.net is a deliberate lookalike of Microsoft's legitimate Azure Static Web Apps domain (azurestaticapps.net). The substitution of apps → provider and the use of the sh subdomain is designed to pass a casual inspection of firewall or proxy logs as legitimate Azure traffic.
If the bootstrap host is not already an IP address, the payload tries to resolve it using 1.1.1.1 first and 8.8.8.8 second. At the time of publication, this IP was 37.16[.]75.69. It then uses the resolved address plus port 443 as the DNS server for TXT exfiltration. If bootstrap resolution fails, it falls back to the raw bootstrap string and still proceeds to build and write the archive before the DNS exfiltration attempt fails.
The cloud value is hardcoded to none; no cloud metadata service probing was observed.
The footer JSON is:
{
"done": true,
"dataChunks": 0
}
DNS chunking is fixed:
Header chunks: 63 hex characters per label
Data chunks: 31 encoded body characters, then hex-encoded into the DNS label
Batch size: up to 160 DNS requests per Promise.all() window
DNS timeout: 8,000 ms
For a 500 KiB gzip archive, the expected data query volume is roughly 29,400 TXT queries, plus header and footer queries. That query count varies with the final compressed archive size. A burst of high-volume TXT queries matching xh, xd, and xf label prefixes under bt[.]node[.]js is a strong detection opportunity.
Authentication and Encoding
The decoded authentication key is:
qZ8pL3vNxR9wKmTyHbVcFgDsJaEoUi
The body encoding chain is:
gzip archive
to base64 text
convert base64 text to bytes
XOR with SHA-256 keystream
base64 encode the XOR output
substitute characters using a key-derived alphabet permutation
split into 31-character body chunks
hex encode each chunk for DNS label transport
The dynamic run confirmed this path by reassembling intercepted xd chunks, reversing the DNS-label hex transport, reversing the substituted base64 body encoding, and reproducing the captured gzip archive byte-for-byte.
Signatures use HMAC-SHA256 and are truncated to 12 hex characters:
The payload awaits resolveTxt() calls, but it does not parse returned TXT records or dispatch commands. No download-and-execute stage, command loop, or response-driven C2 behavior was observed in the decoded payload.
Dynamic Validation
A controlled dynamic run forced the child path by setting __ntw=1 and instrumented filesystem, process, and DNS APIs. The run observed:
One gzip archive written under the temp nt-<pid> directory
An attempted unlinkSync() cleanup of that archive
One bootstrap DNS lookup for sh[.]azurestaticprovider[.]net
Header TXT queries using the xh prefix
Data TXT queries using the xd prefix
One footer TXT query using the xf prefix
Reassembling and decoding the intercepted xd DNS chunks reproduced the captured .tar.gz byte-for-byte, confirming that the DNS body chunks carry the gzip archive.
The reviewed artifacts confirm credential and configuration theft from developer machines and CI-like environments that load the CommonJS entrypoint. Confirmed affected local artifacts include:
node-ipc 9.1.6 tarball
node-ipc 9.2.3 tarball
node-ipc 12.0.1 tarball
The malicious CommonJS file hash is identical across the reviewed unpacked package versions. CommonJS consumers using require("node-ipc") trigger the payload. ESM-only consumers using the package import export path do not load the malicious file in the reviewed package metadata, unless another dependency or direct require path loads node-ipc.cjs.
The payload does not establish persistence in the decoded sample. There is no observed cron, launchd, rc.d, service installation, or second-stage download. The operational impact is concentrated in the execution window: collection, archive creation, DNS TXT exfiltration, and attempted cleanup.
npm invalidated all granular access tokens that bypass 2FA after a fresh Mini Shai-Hulud wave compromised 323 npm packages. Staged publishing also entered public preview.