A Malicious npm Package That Defeats `--ignore-scripts`
The indexed-btree npm package hid its loader inside BTree.prototype.set(), so it ran during normal use and no install-time control ever saw it.
The hardening advice most JavaScript teams adopted after the 2025 and 2026 npm worms was
to stop letting packages run code at install time. The indexed-btree package
walked past that control by not using it. It declared no preinstall and no
postinstall script, and put its loader inside
BTree.prototype.set(), a method that ordinary application code calls every time
it inserts a value. The malware ran when the library was used, not when it was installed,
and the
Cloud
Security Alliance research note on the campaign makes the consequence plain: install-time
controls cannot see a payload that never touches install time.
The package has been removed from npm, along with the GitHub organisation behind it. The reason to read the incident now is that the technique generalises.
The loader shipped inside a method your application calls
Checkmarx published its analysis of the campaign on 17 September 2026. The package
impersonated sorted-btree, a legitimate B-tree and indexing utility, and the
resemblance was close enough to be useful. Checkmarx's description of the design is direct:
the package did not rely on preinstall or postinstall at all, and
instead ran entirely from application code at runtime.
The trigger sat in BTree.prototype.set(). That is not an edge case or a
rarely visited branch. It is the method a developer calls to put something into the data
structure, which means the payload activated during expected use by a project that had done
nothing unusual.
Once triggered, the loader reached for a second file, sharedLoad.min.js,
which carried the obfuscated first stage of the malware. None of that involves a lifecycle
hook, a Git-sourced dependency or a remote-URL tarball, which are the three things npm v12
moved to a deny-by-default posture in June 2026. A package that never uses those mechanisms
is not touched by the control built to police them.
What it did once it ran
Reporting from The Hacker News and the CSA note describes the payload fingerprinting the host, collecting operating system details, hostname, CPU information, memory and system uptime. It beaconed that data to a hardcoded Slack channel and Telegram bot, which is an uncomfortable choice of infrastructure because both services are ordinary traffic on many networks.
For its second stage it used EtherHiding, storing encrypted payload components in a smart contract deployed on the Ethereum Sepolia testnet and retrieving them through the blockchain rather than through a conventional domain or IP address. The Hacker News publishes the contract address, which is useful as an indicator rather than as a curiosity. The technique was documented by Google's Mandiant and Threat Intelligence Group researchers in work on the financially motivated cluster UNC5142, and Google has since reported its use by the North Korea-linked group UNC5342. The attraction is straightforward: a public blockchain cannot be seized or taken down the way a hosting provider or registrar can act against a domain.
The malware also used an ECDH key exchange with a hardcoded X25519 public key to encrypt traffic to its second-stage infrastructure. After execution it deleted its own artifacts and removed the trigger from the package code. That cleanup step is the part with the longest consequences for anyone investigating after the fact.
Ten related packages, and a download count that needs care
Checkmarx tied indexed-btree to a set of related packages, all since removed
from the registry: ordered-kv-index, btree-leaderboard,
priority-slot-queue, btree-range-store, btree-core,
btree-time-index, btree-lru-cache, neighbor-key-map,
sliding-score-window and mutex-forge.
The download numbers in the reporting need to be read carefully, because two different
figures get conflated. The CSA note records that the npm registry reported nearly two
million weekly downloads for indexed-btree around the time it was pulled on
approximately 3 September 2026, and states explicitly that this is a registry-reported
snapshot rate rather than a cumulative total. The cumulative figure belongs to the wider
family, which exceeded 5.3 million downloads over roughly eleven weeks, with
btree-core alone at an estimated 1.95 million and btree-time-index
above 425,000. Adding the weekly rate to the cumulative family total double counts, and the
CSA note says so.
The package was first published to the registry on 18 June 2026 by an account using the
handle charlessadler25. The CSA note records that the account's GitHub
presence, including a commit history and profile image, looked credible rather than like a
hastily assembled throwaway, and assesses that this was likely constructed to lend it an
air of legitimacy. That assessment is CSA's, and no public attribution of the operators has
been made. Blockchain analysis cited in the reporting puts the campaign's proceeds at
approximately 109 ETH, around €231,000 at the time of reporting. The Hacker News gives the
precise figure as €230,933.57.
How to check whether it ran in your tree
The packages are gone from the registry but they are not gone from lockfiles, build caches or container images, and the installation window to search is 18 June to 3 September 2026.
# Search manifests and lockfiles for the package family.
grep -rn -E "indexed-btree|btree-core|btree-time-index|btree-range-store|ordered-kv-index|btree-lru-cache|neighbor-key-map|sliding-score-window|mutex-forge|priority-slot-queue|btree-leaderboard" \
package.json package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null
# A zero return here does not clear the host: the payload deletes its own artifacts.
Absence of evidence on disk is not evidence of absence, and this is the rare case where that phrase is a literal technical statement. The malware removed its own files after execution, so a host that was compromised in July can look clean today. The CSA note advises giving log-based and network-based indicators priority over filesystem inspection, and cross-referencing the published indicators, including the Sepolia contract address, the RPC endpoints and the Telegram bot token, against network and DNS logs.
The traffic to look for is outbound calls to public Ethereum RPC providers from build runners and developer workstations. Those hosts have little ordinary reason to talk to a blockchain node, and that traffic is what the second stage depends on.
For anything that did run the affected code, the CSA note's advice is to treat the
environment as compromised and rotate the credentials reachable from it: cloud provider
keys, CI/CD tokens, and any model provider or AI service API keys on the same host. The AI
toolchain angle in that guidance is the same one that applies to the tensorlake
compromise. Build runners aggregate cloud keys, registry tokens and model provider keys in
one place, which makes them worth more than their own contents.
Why --ignore-scripts did not stop it
Blocking lifecycle scripts is still worth doing. Miasma and the earlier Shai-Hulud waves
used preinstall hooks to harvest multi-cloud credentials the moment a package
was installed, and npm v12's deny-by-default posture closes that path for installs that
respect it. The mistake is treating the control as complete.
The CSA note's framing is that security controls change attacker behaviour rather than eliminate the underlying threat. Blocking lifecycle scripts removes one execution path; the operators moved to another. The note also records an earlier campaign that shifted its dropper out of the lifecycle script and into the package's main code when script inspection started catching it. The evidence trail points the same way twice.
The practical addition is runtime behaviour monitoring of dependency code, or at minimum
a check on whether your existing tooling does any. Software composition analysis that scans
for suspicious preinstall and postinstall hooks will not surface a
trigger hidden in a widely called method, and static review of a package's declared scripts
and manifest metadata will not either. If nothing in your stack watches what a dependency
does when it runs, that is a documented gap rather than an assumed capability.
Two cheaper controls are worth pairing with it. Restricting outbound network access from build runners to known destinations would have flagged or blocked the beaconing to Slack, Telegram and the Sepolia RPC endpoints. And a minimum release age on new package versions buys the days that detection usually needs, since this campaign ran from June to September before it was identified.
The uncomfortable part of the story is that the package looked like a utility and behaved like one. It built a working B-tree. The malicious code simply travelled inside a method that did exactly what it claimed to do.
Primary sources
- https://labs.cloudsecurityalliance.org/research/csa-research-note-npm-runtime-execution-supply-chain-2026092/
- https://thehackernews.com/2026/09/malicious-npm-package-indexed-btree-hid.html
- https://checkmarx.com/zero-post/npm-btree-malware-campaign-affects-millions-of-downloads-no-need-for-install-script/
More from the wire
An SSRF in the Official MCP Fetch Servers
CWE-918 in mcp-server-fetch and mcp-server-everything 2026.6.0 to 2026.6.4 lets a crafted URL steer the fetch tool, and the fix pull request is still unmerged.
The tensorlake npm Worm Targets AI Tools, and Waits for a Token Revoke
Version 0.5.144 of the tensorlake npm SDK shipped a preinstall worm that wipes the machine if a stolen GitHub token is revoked.
GitHub Starts Blocking Committed Anthropic Keys by Default
Anthropic service and user API keys are now in push protection defaults, so a commit carrying one is refused before it reaches the repository.