Skip to main content

How to Detect npm Encrypted Loader Malware: 3.1M Downloads

Detect npm encrypted loader malware before it runs: the trigger matrix, AES-256-GCM payload, and IOCs behind mathmain's 3.1M weekly downloads.

9 min read
Dashboard-style cover showing the mathmain npm attack's four-stage activation chain and its 3.1 million weekly download scale

TL;DR Detect npm encrypted loader malware like mathmain’s by the pattern, not a signature — its AES-256-GCM payload only decrypts when lusolve() receives one specific 3x3 matrix, a password no scanner would ever guess. Across mathmain and two sibling packages, that trick pulled 3.1 million downloads in one week before SafeDep flagged an unused function call and JFrog recovered the trigger. Catch it by diffing tarballs against source, grepping for decrypt-then-require patterns, and denying network egress by default.

What Is the mathmain npm Supply-Chain Attack?

mathmain is an npm package that copies the popular mathjs library almost exactly, except for one addition: an encrypted, dormant remote-access implant. Version 1.0.1, published August 27, 2026, looks and behaves like mathjs for every normal use — it only becomes malicious when a specific function is called with a specific, unusual input. Two related packages, mathsbase and math-universe, carry the same mechanism, and between September 12 and 18, 2026 the three together logged 605,157, 1,923,059, and 569,729 weekly downloads — 3.1 million installs of malware that most of those installs never triggered.

That “never triggered” part is the design. Most npm malware runs the moment the package loads or a preinstall script fires, which is exactly the behavior automated scanners watch for. mathmain’s payload stays encrypted on disk until a caller supplies the one input that derives its decryption key, so a sandbox that installs the package and pokes at a few obvious functions sees nothing wrong.

How the Encrypted Loader Actually Works

The activation chain has five links, and every one of them looks like ordinary library code until you know what to look for:

  1. An attacker (or anyone who knows the trick) calls lusolve() — the standard linear-solver function in mathjs and its clones — with the 3x3 Pascal matrix [[1,1,1],[1,2,3],[1,3,6]].
  2. The solver’s internal call path reaches removeSolveValidation() inside lib/cjs/utils/is.js, a function with no visible legitimate caller anywhere in the public API.
  3. That function converts the matrix’s LU-decomposition factor — [[1,0,0],[1,1,0],[1,0.5,1]] — into a JSON string and uses it as a password.
  4. A helper named validEvent() runs AES-256-GCM decryption against a fixed on-disk layout — a 16-byte salt, a 12-byte IV, a 16-byte auth tag, then ciphertext — using a key derived from that password with scrypt.
  5. Once decrypted, event() writes the plaintext to disk and require() loads and executes it immediately.

What comes out is three stages. graph.js (20,918 bytes of ciphertext) fingerprints the host through Node’s os and fs modules, generates an X25519 keypair, and can execute shell commands. bignumber/type.js (1.18 MB of ciphertext) bundles the ethers.js library for blockchain interaction.

fraction.js (9,084 bytes of ciphertext) is the command-and-control agent, polling Slack’s conversations.history endpoint every ten seconds for new instructions and falling back to Telegram as a second channel. Commands can also arrive through a smart contract on the Base Sepolia testnet, reached via Infura or Alchemy RPC endpoints. Both the Slack and blockchain deployments were traced to the same Alchemy project key, tying the whole operation to one actor.

How to Detect npm Encrypted Loader Malware Before It Runs

Signature-based scanning cannot catch this — there is no malicious string to hash-match until the payload is already decrypted in memory. What actually catches it is looking for the shape of the technique rather than its contents:

  • An internal function with no public caller. removeSolveValidation() exists in mathmain’s source but nothing in its documented API ever calls it. That mismatch — dead code that isn’t actually dead, because something calls it from outside the package — is the single strongest signal, and it’s exactly what led SafeDep to the loader on September 17, 2026.
  • Decryption primitives paired with a nearby require() or dynamic eval. Legitimate libraries rarely decrypt their own source at runtime. crypto.createDecipheriv (or an scrypt-based key derivation) sitting next to code that immediately requires or executes the result is a pattern worth flagging in any dependency, regardless of what library it claims to be.
  • A tarball that doesn’t match its source repository. mathmain’s GitHub repository never contained the loader files — they exist only in what npm actually serves. This is the check every team can run today without waiting on a specific IOC feed.

Why a Pascal Matrix Is the Only Key That Unlocks It

The trigger choice is what makes this attack hard to catch in a sandbox rather than what makes it hard to reverse-engineer once found. A Pascal matrix run through LU decomposition is unremarkable-looking linear algebra — nothing about calling lusolve([[1,1,1],[1,2,3],[1,3,6]]) looks adversarial, and a fuzzer that calls the function with random matrices has a vanishingly small chance of ever landing on the exact values that decrypt anything. JFrog recovered the specific trigger on September 21, 2026, four days after SafeDep’s initial discovery — meaning even security researchers who knew something was wrong needed several more days to find the one input that proved it.

Five-stage activation chain of the mathmain npm attack, from installing the package through calling lusolve with the trigger matrix, deriving an AES-256-GCM key via scrypt, decrypting the payload, and requiring it into the running process

mathmain vs Other npm Supply-Chain Attacks: A Comparison

Trigger-gated activation is a step change from how earlier high-profile npm attacks worked. Each of the well-documented incidents below ran its payload unconditionally, once installed — mathmain is the first widely reported case where the payload waits for a specific application-level input:

AttackTriggerHow It Got InC2 / Exfil Channel
mathmain (2026)One specific matrix passed to lusolve()Typosquat of mathjsSlack + Telegram + blockchain RPC
event-stream (2018)Runs once bundled into the target appMaintainer handoff via social engineeringDirect network call to steal wallet keys
ua-parser-js (2021)Runs on npm install via a preinstall scriptCompromised maintainer accountMiner + credential stealer, direct execution
node-ipc (2022)Runs unconditionally, gated on IP geolocationMaintainer added it directly (“protestware”)None — local file overwrite only

The earlier three all fire the moment the package lands, which is why install-time monitoring and preinstall-script auditing became standard defenses. mathmain doesn’t need a preinstall script at all, so those defenses don’t see it — the industry’s detection tooling was built for the previous generation of this attack.

5 Steps to Audit Your Dependencies for Hidden Loaders

  1. Diff the installed tarball against the package’s public source repository. If a file exists in what you installed but not in the linked GitHub repo, that alone is worth an incident review — this one check would have caught mathmain without any prior knowledge of it.

  2. Grep node_modules for decryption primitives sitting next to a require() or eval(). Look for createDecipheriv, scrypt, or similar key-derivation calls followed by code that loads whatever they produce.

  3. Flag internal functions with no reachable caller in the package’s own public API. A function that exists but that nothing in the documented surface ever invokes is either dead code or a hook for something external — treat it as the latter until proven otherwise.

  4. Pin dependencies and add a cooldown window before installing a fresh version. The same principle behind gem cooldown on RubyGems applies here: refusing to resolve a package version until it has been public for several days gives the community time to flag it first.

  5. Deny network egress by default in build and CI environments, then alert on any attempt. mathmain’s payload needs to reach Slack, Telegram, and an RPC endpoint to do anything useful. A dependency that has no legitimate reason to make outbound calls, making one, is a detectable event even if you don’t recognize the destination.

Bar chart showing mathmain's malicious npm package family logged 3.1 million downloads in one week — mathsbase at 1.92 million, mathmain at 605,157, and math-universe at 569,729

What Breaks If You Only Scan for Known Malware Signatures?

Signature and hash-based scanning is still worth running — the IOC hashes below will catch anyone who already has this exact payload on disk. But it only ever catches known threats, and mathmain was unknown for three weeks after it shipped: published August 27, discovered September 17, fully understood September 21. A team relying only on signature matching had no defense for those three weeks, because there was no signature yet to match. The structural checks in the previous section — dead-code detection, tarball-vs-source diffing, egress monitoring — catch the pattern of this attack independent of which specific package instantiates it next, which matters because mathsbase and math-universe prove the same actor already reused the technique twice.

Text
Loader files (SHA-256):
  lib/cjs/utils/event.js  ab66c98e8ed5235feb963ec8845765f62f5f26b1c58c266c409767e53bcb5ccd
  lib/cjs/utils/is.js     5d9e952c51875d2b897eedc22b002b94ab21c8004d513bc99ce3a885f8a01dae

Decrypted payload (SHA-256):
  graph.js                1e0f09c84aaf573627c003ce0f086517c3ea980cbea02f8ff918b1cc0d7e0bbb
  fraction.js              6fd655d7196880fc5783f9dbb62b428baf220c2781970be54044376330be7af3

mathmain@1.0.1 archive:    1723a0df210ac61281a504f3a07ec3605d20151631e0635cc344cacc71019135

Five-step defense flow for auditing npm dependencies: diff tarball against source, grep for decrypt-then-require patterns, flag uncalled internal functions, pin with a cooldown window, and deny egress by default

FAQ

How do I know if I have mathmain, mathsbase, or math-universe installed? Grep your lockfile and node_modules for those three package names — they are typosquats of the popular mathjs library, and mathmain@1.0.1 published August 27, 2026 is the confirmed malicious version. Also check for the loader files lib/cjs/utils/event.js and lib/cjs/utils/is.js, since a legitimate mathjs install never ships those paths.

Is this attack specific to npm, or could it happen on PyPI or crates.io too? The trigger-gated decryption pattern is package-manager-agnostic — any ecosystem that lets a package ship arbitrary code executed on require or import can hide a payload behind a rare function call. The specific trigger here, a matrix passed to mathjs’s lusolve() solver, only exists because mathmain copied that one library, but the same technique works against any sufficiently used function in any language’s package registry.

Why did the attackers use a math trigger instead of a normal command-and-control check-in? Because static and dynamic scanners execute packages broadly, not with attacker-chosen inputs, so a payload gated behind one specific 3x3 matrix never fires during automated analysis. A sandbox that imports mathmain and calls a few obvious functions will never stumble onto the exact trigger, which is the whole point — the malicious code only activates for someone who already knows the password.

Does removing mathmain from node_modules fully remove the threat? Removing the package stops the loader from running again, but if lusolve() was ever called with the trigger matrix while the package was installed, treat the host as compromised. Rotate any secrets the process could reach, and look for the decrypted payload files — graph.js, bignumber/type.js, fraction.js — since their presence anywhere outside the original encrypted archive means the loader already ran.

Can a lockfile alone have prevented this? A lockfile pins versions once it’s written, but it does not stop the first npm install, or an unpinned CI run, from resolving mathmain@1.0.1 the moment the attacker publishes it. Lockfiles need to be paired with a cooldown window before a fresh version is ever installed, plus the audit steps above, to actually block a same-day supply-chain attack.

What’s the fastest single check if I don’t have time for the full audit? Diff your installed package’s files and hashes against what the project actually publishes on GitHub. mathmain’s malicious loader files exist only inside the npm tarball and never appear in its public source repository, so a tarball-versus-source diff surfaces this entire class of attack in seconds, without needing to know any package-specific indicator of compromise in advance.

For related hardening, see how a package registry can hand out remote code execution through its own build pipeline, why pinning and reviewing AI-generated code changes closes a similar review gap, and how egress controls stop a compromised dependency from phoning home even after it runs. If you’re auditing at the CVE level rather than the package level, the libheif AVIF decoder RCE and the vLLM inference CVE walkthrough follow the same diff-and-verify discipline. And before you trust any third-party incident write-up — this one included — it’s worth knowing how to tell a real CVE report from AI-generated slop. Grouping and reviewing routine dependency bumps is easier with Dependabot’s grouped-update mode, which at least shrinks the surface you have to audit by hand.

Sources

Frequently asked questions

Share this article:
X LinkedIn

Google Search · Preferred sources

Prefer this site on Google

If you already read this writing, add umesh-malik.com as a Preferred Source. Google can then highlight it with a preferred badge in Top Stories, AI Overviews, and AI Mode — for you, not as a site-wide ranking boost.

Keep reading

Get new posts on AI, Claude Code & LLMs

New deep-dives on AI engineering, Claude Code, and developer tooling — follow along however you prefer.