Skip to main content
The state of ai impact assessment
979 Downloads, Six Replacements: Why Your npm Controls Keep FailingSoftware Supply Chain Security
6 min readFor TPRM Practitioners

979 Downloads, Six Replacements: Why Your npm Controls Keep Failing

When npm removed two malicious packages on September 28, 2026, the threat actor behind MALFEX published replacements within 36 hours. When those were removed, another variant appeared. By October 11, the campaign had evolved through six distinct package iterations, each adapting to detection and removal. If your organization's open-source risk controls rely on blocklists and one-time scans, you're already behind.

Why These Mistakes Keep Happening

The MALFEX campaign, active since August 6, 2023, exposes a fundamental misalignment between how your team assesses open-source components and how modern supply chain attacks operate. Most vendor intake workflows treat npm packages as static artifacts: you scan at ingestion, check against known-bad lists, and approve. The threat model assumes adversaries publish once and wait. MALFEX demonstrates the opposite: rapid iteration, namespace rotation, and tactical response to takedowns within hours.

Your controls fail because they're designed for quarterly vendor reviews, not for adversaries who publish three replacement packages in 48 hours.

Mistake 1: Treating Package Registries as Pre-Vetted Sources

Why it happens: Teams assume npm's publishing process includes meaningful security vetting. The registry is treated as semi-trusted infrastructure, similar to an enterprise app store.

The consequence: The package "imgdecode" was uploaded September 30, 2026, and downloaded 979 times before broader detection. It remained available on npm as of the source reporting date. Your developers pulled it because it appeared in search results and had a plausible name. No alarm fired because the registry itself wasn't flagged as hostile.

The fix: Implement allow-listing for production dependency chains. Maintain an internal mirror or proxy (Artifactory, Nexus, Verdaccio) that only admits packages after human review for critical paths. For non-critical dependencies, require:

  • Minimum package age (e.g., 30 days since first publication)
  • Minimum download threshold from established projects
  • Verified publisher with history (not accounts created within the last 90 days)

MALFEX used five publisher handles (malfexkkj, malfex_user, malfexteste2, malfexteste3, malfexteste4). A policy requiring publisher account age over six months would have blocked all of them.

Mistake 2: Scanning Only at Intake, Not at Runtime Trigger Points

Why it happens: Static analysis tools run during CI/CD or at package installation. Once a dependency passes intake, it's trusted. The assumption is that require() statements execute benign code.

The consequence: The Node.js stealer chain used by MALFEX featured no install hooks. Malicious code executed when developers called require('native-runner') in application code. Traditional install-time scanners saw nothing suspicious because the payload triggered at runtime, not during npm install. Your security gate passed the package, and the compromise happened during normal application execution.

The fix: Extend monitoring to runtime behavior, not just installation artifacts. Deploy:

  • Application-layer monitoring that flags unexpected child processes (the stealer spawned "node_runtime_helper.exe" in %APPDATA%)
  • Network egress rules that block Node.js processes from initiating outbound connections to non-approved destinations (MALFEX contacted 104.234.65[.]75:700 and :7007)
  • Filesystem integrity monitoring for writes to %APPDATA%\Microsoft\Windows\ by Node.js processes

Pair static dependency scans with runtime sandboxing for any package that loads native addons or compiled DLLs. The "imgdecode" package used a 64-bit MSVC DLL to inject shellcode into dllhost.exe. That behavior is detectable, but only if you're watching process execution, not just file contents.

Mistake 3: Failing to Monitor Removed Dependencies for Replacements

Why it happens: When npm removes a malicious package, teams treat the incident as closed. The package is blocklisted, tickets are resolved, and attention moves elsewhere.

The consequence: MALFEX published "runhelper" and "imgbundle" within two days of npm removing "native-runner" and "img-to-native." The replacement packages restored the attack chain using slightly different names and dependency structures. If your team blocklisted the original packages but didn't monitor for functional equivalents, developers could have pulled the replacements without triggering alerts.

The fix: When a dependency is removed for malicious behavior, implement active monitoring for:

  • New packages from the same publisher handle
  • Packages with similar names or descriptions (edit distance < 3 characters)
  • Packages that replicate the dependency graph of the removed component (e.g., "imgbundle" replaced "img-to-native" and was called by the same parent package)

Automate this with a threat intelligence feed that tracks npm takedowns and correlates them with new publications. MALFEX's timeline shows a clear pattern: removal at 4:06 p.m. UTC, replacement by 3:29 a.m. UTC. That's a nine-hour window. Your monitoring cadence must be faster than the adversary's replacement cycle.

Mistake 4: Ignoring Compiled Native Addons as High-Risk Artifacts

Why it happens: Native Node.js addons (compiled C/C++ modules) are treated as performance optimizations, not security boundaries. Reviews focus on JavaScript source, which is readable. Compiled binaries are opaque, so they're either trusted or ignored.

The consequence: The "imgdecode" package hid its loader in a compiled DLL. The JavaScript wrapper was nine lines. Static analysis of the JS file revealed nothing. The DLL downloaded encrypted shellcode from GitHub, decrypted it using the key "nif-runtime-2027," and injected the payload into dllhost.exe. Your source code review passed because the malicious logic was in the binary, not the script.

The fix: Classify any package containing native addons (.node files, .dll, .so, .dylib) as high-risk and subject to enhanced due diligence:

  • Require source code availability for the native component (reject pre-compiled-only packages)
  • Run binaries in an isolated sandbox and observe system calls (file writes, network connections, process injection)
  • Use dynamic analysis tools (e.g., Cuckoo Sandbox, ANY.RUN) to execute the addon and capture behavior

For packages like "imgdecode," dynamic analysis would immediately reveal the GitHub fetch, decryption routine, and dllhost.exe injection. None of that is visible in static JS analysis.

Mistake 5: Assuming Single-Package Threats Instead of Multi-Package Chains

Why it happens: Security tooling scans individual packages for known vulnerabilities or malicious patterns. The mental model is "one package = one threat." Multi-package attack chains, where malicious functionality is distributed across dependencies, evade this detection.

The consequence: The MALFEX Node.js stealer required three packages working together: "cdn-img-fetch" downloaded an encrypted blob, "img-to-native" decrypted it and wrote a Go executable to disk, and "native-runner" executed the binary. Each package, analyzed in isolation, might appear benign or only suspicious. The full attack chain only materialized when all three were present. Your scanner flagged nothing because it evaluated each package independently.

The fix: Implement dependency graph analysis that evaluates risk at the chain level, not the package level. Look for:

  • Packages that write executable files to disk and other packages that execute files from the same path
  • Dependency chains where one package fetches remote content and another decrypts or executes it
  • Unusual dependency relationships (e.g., "img-to-native" listed "img-to-native" as its own dependency, a recursive structure with no legitimate use case)

Graph-based analysis tools (Snyk, Socket, Phylum) can map these relationships. Configure alerts for any chain that combines network retrieval, cryptographic operations, and process execution across multiple packages.

Prevention Checklist

Use this checklist to audit your open-source supply chain controls:

  • Allow-list enforcement: Production dependencies pulled only from internal mirror with approved packages
  • Publisher vetting: Reject packages from accounts < 90 days old or with no publication history
  • Minimum package age: Require 30+ days since first publication for non-critical dependencies
  • Runtime behavior monitoring: Alert on unexpected child processes, %APPDATA% writes, or outbound connections from Node.js
  • Native addon policy: All packages with compiled binaries subject to sandbox execution and behavioral analysis
  • Dependency graph analysis: Automated scanning for multi-package attack chains (fetch + decrypt + execute patterns)
  • Takedown monitoring: Active tracking of npm removals with automated searches for replacement packages from same publisher or similar names
  • Incident response integration: Playbook for developer machines that loaded malicious packages (isolate system, check Registry Run keys and scheduled tasks, rotate credentials, audit for persistence as detailed in the MALFEX IOCs)

The MALFEX campaign published six package variants in six weeks, adapting to each takedown within hours. Your controls must match that cycle time, or you're conceding the initiative to adversaries who iterate faster than your quarterly reviews.

a promotional banner asking how ready are you for PCI DSS 4.0? With a call-to-action to get the checklist now.

You Might Also Like