In-app reader
20 min read
Related Products Advanced DNS SecurityAdvanced URL FilteringAdvanced WildFireCloud-Delivered Security ServicesCortexCortex CloudCortex XDRCortex XSIAMUnit 42 Cloud Security AssessmentUnit 42 Incident Response
By:
Published: August 6, 2026
Categories:
Tags:
Share
A self-propagating npm worm nicknamed ChainDrop infected over 400 packages that are collectively downloaded hundreds of millions of times each week. This includes malicious versions of widely used packages such as keyv and cacheable-request . Unit 42 has unique observations of this attack.
The attackers behind ChainDrop potentially exposed developer workstations, continuous integration (CI) pipelines, cloud environments and downstream software users across a large number of organizations.
Once installed, ChainDrop steals:
Cloud credentials
npm and GitHub tokens
SSH keys
Other sensitive developer data
It can also extract temporary credentials from GitHub Actions runner memory and use stolen npm publishing tokens to infect and republish additional packages while preserving their legitimate functionality.
We have observed active attempted operations, which were detected out of the box by our existing products.
During our investigation into this attack, we identified 453 public GitHub repositories across five accounts matching the worm’s exfiltration patterns. We also detected ChainDrop execution across 10 distinct environments. At the time of publication, these repos were removed.
We have deobfuscated the malware and identified:
Persistence through developer and AI coding tools
Blockchain-based command-and-control (C2) resolution
Its ability to execute additional attacker-supplied code
Additionally, late on Aug. 4, 2026, we observed the adversary silently reconfiguring the worm's entire C2 infrastructure through a single Ethereum transaction, without requiring any update to the deployed malware.
This attack is the latest in a series of threats to the security of the npm ecosystem.
Unit 42 recommends:
Identifying installations of affected npm package versions
Removing affected package versions
Investigating developer workstations and CI runners for signs of compromise
Reviewing unexpected npm publishing and GitHub repository activity.
Revoking and rotating potentially exposed npm, GitHub, cloud, SSH and automation credentials.
Removing identified persistence mechanisms
Blocking both the domain-based and GitHub-based exfiltration channels
The Koi Agentic Endpoint Security risk engine flagged the malicious package activity as the attack unfolded. Cortex XDR detected and alerted on the worm’s execution using out-of-the-box behavioral detections.
Palo Alto Networks customers can use Koi Agentic Endpoint Security to help identify and control malicious packages across developer endpoints.
The Cortex AgentiX Threat Intel agent can help allow analysts to extract, enrich, and search IoCs using natural language to quickly determine organizational impact.
Cortex Cloud Endpoint Protection leverages AI-enabled analytics to help detect and prevent threats targeting Linux endpoints, containers, and associated cloud IAM policies.
Cortex XDR and XSIAM provide behavioral detection, investigation and response that can help organizations address ChainDrop activity executing in development environments.
Idira Secrets Manager and Secrets Hub eliminate hard-coded credentials from configure files and sour ce code by automating zero-downtime rotation, and dynamically delivering just-in-time access to non-human identities across multi-cloud and DevOps environments.
The Unit 42 Cloud Security Assessment is an evaluation service that reviews cloud infrastructure to identify misconfigurations and security gaps.
The Unit 42 Incident Response team can also be engaged to help with a compromise or to provide a proactive assessment to lower your risk.
Related Unit 42 Topics AI, Malware, Supply Chain, npm Packages
We analyzed the contents from one of the infected packages to understand the full attack chain.
The package contained the legitimate software development kit (SDK) code that a user would expect, including the source, dependencies and documentation. But it also contained small indicators of the ChainDrop worm: two extra top-level files and one lifecycle hook.
The indicators of the worm can be subtle, as illustrated in the following example.
One of the indicators is an infected npm package's package.json file containing code with the preinstall command, as shown in Figure 1.
That preinstall line is the only modification the worm makes to this package's manifest. It points to setup.mjs , a dropper that checks whetherBun (a lightweight JavaScript runtime and package manager alternative to Node.js) is on PATH . It downloads Bun 1.3.13 from the legitimate Oven GitHub repository if it isn't present. Then it feeds Bun a 727 KB obfuscated JavaScript payload ( math_init.js ) compressed into two source lines.
To be clear: Bun is not compromised. The attacker is using a legitimate runtime as a portable execution vehicle.
The payload spawns a detached background process, sets _NODE_RUNTIME_INIT=1 to prevent recursive relaunch and lets the install finish cleanly. No errors. No warnings.
Most developers would move on without noticing a key detail: The worm is already running.
The worm detaches when it is not in CI. If it detects a CI environment it runs inline in the job instead, which means its own debug output lands in the workflow log. This is useful for defenders looking for indicators because the worm is chatty.
One further gate runs before the worm engages in any collection. This gate is a locale check that, on a Russian-language host, prints “Exiting as russian language detected!” and exits cleanly. The worm spares those machines.
The background payload begins a sweep of the infected machine to harvest credentials from the environment. These include the following categories:
**Cloud credentials: **
Multiple major cloud infrastructure platforms
The worm queries metadata endpoints and token endpoints across both compute instances and container services to harvest temporary identity and access management (IAM) role credentials, extending scope to short-lived identity tokens used by automated integration runners
Developer tooling:
Docker and Helm configurations
Git credentials
Mount listings
npm and GitHub tokens
Poetry and PyPI credentials
RubyGems tokens
SSH keys
Terraform state
Vault tokens
AI tools:
AI-assisted coding tools
Cloud-based development platforms
Open-source coding assistant configurations and authentication artifacts
Everything else:
.env files
.netrc
Application configuration scattered across the home directory
Bitcoin and Electrum wallet files
Jenkins encrypted credential material
Kubernetes service-account tokens and kubeconfigs
Shell histories
ChainDrop harvests credentials, but also a wide variety of other information about the systems and environment it’s running on.
Some of the information stolen is vital for the worm’s survival. The npm and GitHub tokens it finds are what it needs to keep spreading.
An embedded Python helper hidden inside an encrypted blob in the payload locates the Runner.Worker process on GitHub Actions runners, opens /proc/<pid>/maps and /proc/<pid>/mem , and searches live process memory for OpenID Connect (OIDC) tokens and runner secrets.
The flow of this GitHub Actions runner memory scraping is illustrated in Figure 2.
Rather than waiting for a file to be written to disk, the worm searches memory. In the process, it captures secrets that may have been designed to vanish when a job finishes.
Organizations should be aware that CI runners are credential targets and can be exfiltrated through attacks on process memory.
The worm establishes several persistence mechanisms, but two of them deserve special attention:
Cross-linked persistence through VS Code and Claude Code
A latent capability for OS-level persistence
It writes a .vscode/tasks.json file with a task labeled Environment Setup and sets it to run when the folder opens — meaning it executes automatically whenever a developer opens the project in VS Code. That task runs node .claude/setup.mjs , a copy of the dropper that is byte-identical to the setup.mjs shipped in the package itself.
It also writes a .claude/settings.json file with a SessionStart command hook, meaning it executes whenever Claude Code starts a session in the project. That hook runs node .vscode/setup.mjs , a second copy of the same dropper.
Figure 3 shows the cross-linked persistence through both .vscode/tasks.json and .claude/settings.json files.
Neither file triggers the other. Each one runs the dropper copy sitting in the other's directory, and the actual trigger in both cases is a developer action: opening the folder, or starting a Claude Code session. Cross-referencing is a naming trick that makes each artifact look like it belongs to the other tool.
The payload is only ever written as .claude/math_init.js , and setup.mjs resolves math_init.js relative to its own location. .vscode/setup.mjs goes looking for a .vscode/math_init.js that the malware never dropped. In this build, only the VS Code path reaches a payload at all.
The full set of dropped files is:
.claude/math_init.js
.claude/settings.json
.claude/setup.mjs
.vscode/setup.mjs
.vscode/tasks.json
Deleting either directory outright breaks both paths. However, defenders should remove all five files to be sure the worm is disabled.
The worm also carries an installer for a macOS LaunchAgent ( com.user.gh-token-monitor ) and a Linux systemd user service ( gh-token-monitor.service ). In this sample, the installer was decrypted but never invoked. The routine that pipes it to bash has no call site, so treat OS-level persistence as latent capability, not observed behavior.
The attacker is turning a trusted developer and AI-tool configuration into execution infrastructure. These aren't files most developers think to audit.
Once the worm has an npm token, it:
Identifies every package the account can publish
Downloads or reconstructs each package
Adds preinstall: node setup.mjs to the package.json file
Writes the dropper ( setup.mjs ) and the obfuscated payload ( math_init.js )
Increments the patch version
Republishes the infected package as the current npm package
The infected package still works. The original source code is intact. As in the sample we analyzed, the only additions are the two top-level files and the lifecycle hook.
The worm also plants a .github/workflows/codeql_analysis.yml file that serializes ${{ toJSON(secrets) }} and uploads it as an Actions artifact, another path to exfiltrate repository secrets. And it creates public repositories under the victim's GitHub account with the description Shai-Hulud: Here We Go Again and Dune-themed names, using them as an additional exfiltration channel.
Everything above is a relatively loud and more obvious propagation path. There is a second typosquatting method that is much quieter and it only appears in a single place.
Before collecting anything, the worm checks three environment variables. If these three variables are set:
GITHUB_ACTIONS
GITHUB_REPOSITORY to contain /opensearch-js
GITHUB_WORKFLOW_REF to contain release-drafter.yml
The worm runs a static routine of republishing the repo and exits. No collection takes place.
Also, If the worm is placed in a repo that contains /opensearch-js , but does not contain release-drafter.yml , it exits and steals nothing at all. It stays silent in the runs a maintainer is most likely to be reading.
Inside this second method, the worm does not need a stolen npm token. It asks the runner for an OIDC token with the audience npm:registry.npmjs.org and trades it at npm's own trusted-publishing exchange endpoint for a real publish credential. The repository's legitimate release identity becomes the attacker's.
Then it modifies the package, and not the way it modifies everything else. This path never touches scripts. It downloads the latest @opensearch-project/opensearch tarball, bumps the patch version and adds one line to the package.json file shown below in Figure 4.
The dependency name typosquats the project's own @opensearch-project scope and points at a pinned commit of the project's own repository. In a diff it reads like an internal helper. Detections built around preinstall hooks could easily miss it.
And then the worm signs the result.
Before publishing, the worm:
Requests a second OIDC token (audience sigstore this time)
Obtains a Fulcio certificate
Builds an in-toto SLSA v1 provenance statement over the tarball's SHA-512 hash
DSSE-signs it with an ephemeral P-256 key
Uploads the entry to the public Rekor transparency log
Attaches the bundle to the publish as <name>-<version>.sigstore
Logs the resulting search.sigstore.dev URL as it goes
This is not forged provenance. The attestation says the tarball was built in that repository by that workflow, and that is true.
That breaks a control many teams are currently leaning on. Given the reality of today’s npm supply chain threats, a package having valid npm provenance does not mean the package is clean. It only means the tarball came out of the workflow named in the certificate. If that workflow is running attacker code, valid provenance is what you should expect to see. Pivot on the Rekor log index and the workflow identity inside the certificate, not on whether the signature checks out.
We did not observe this path execute, and it cannot execute anywhere except in release-drafter.yml inside the opensearch-project/opensearch-js workflow. But it is fully implemented, reachable from the payload's main entry point, and it names its target in cleartext once the string layers come off. This repository is not typosquatted. The typosquat is the injected dependency name @opensearch/setup , which imitates the real @opensearch-project scope.
The worm doesn't contain a hard-coded C2 domain. Instead, it calls an Ethereum smart contract to ask where to send stolen data.
The contract sits at 0xE1f2395ee43e45A1556EC6438a88c31B83493103 . This contract is a small StringListStore with three functions: return all domains, return owner and an owner-only setter. It emits no events, so domain rotation is a silent state write. Defenders who block today's domain may not notice when the operator changes it unless they're polling the contract.
The worm rotates through roughly 60 public Ethereum RPC endpoints until one answers, making it resilient to any single provider blocking the request.
When the contract was first configured, the operator wrote three domains:
npm-cache[.]com
pypi-get[.]com
js-mirror[.]com
Two hours and 35 minutes later, they replaced the list with only npm-cache[.]com . As of our analysis, that's still the active C2.
If contract-resolved domains fail, the worm falls back to searching GitHub commits for the marker thebeautifulmarchoftime , expecting a signed record containing a backup domain. During our query, the fallback was unarmed. No valid operator record existed. However, the mechanism is built and waiting.
The primary C2 domain, npm-cache[.]com , sits behind an edge computing and reverse-proxy service, so its published addresses are shared edge addresses rather than attacker-owned hosts, so block on domain or SNI. Blocking these IP addresses will not reach the origin and will affect unrelated traffic.
After sending stolen data, the worm reads the HTTP response, parses it as JSON and evaluates whatever comes back. The JavaScript code to accomplish this is shown below in Figure 5.
There is no fixed second-stage payload baked into the worm. The operator chooses the next stage at request time. Because each exfiltration request includes a host-derived UUID, the response can be targeted per victim and never written to disk.
During our analysis, we sent a correctly formatted synthetic envelope using the worm's exact encryption scheme with dummy data to the live C2 endpoint. The server returned an HTTP 200 OK with an empty body. No code field was served to our probe.
That means the remote code execution (RCE) channel was either disarmed at test time, selectively gated on victim attributes or asynchronous.
Stolen data is JSON-serialized, gzipped, encrypted with a random AES-256-GCM key and wrapped with RSA-OAEP-SHA256 using an embedded public key. The worm sends the code shown below in Figure 6.
Everything goes to hxxps://npm-cache[.]com:443/router over TLS. Network capture can prove that data left the machine and estimate its volume, but recovering the plaintext requires the operator's private RSA key.
Only the domain-based sender evaluates returned code . Blocking the domain prevents an arbitrary RCE stage if the functionality is enabled. But the GitHub fallback can still exfiltrate data through victim-owned repositories, which means full containment requires addressing both channels.
There is a third situation that we describe in this section, and it is the strangest one. When the GitHub sender carries a stolen token, the worm Base64-encodes that token twice and makes the result the commit message, prefixed with a fixed marker:
IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients
A separate routine in the same payload searches GitHub's commit API for that marker, double-decodes every match and keeps any token that passes a repository-scope check. One victim's stolen credentials become a usable resource for every other running copy of the worm.
Despite the claims made in the marker, defenders should grep for it. It is long enough and strange enough that a full match is highly unlikely to be a false positive. A live hit means a credential is sitting in a public commit and needs revoking.
The three C2 domains were registered through one registrar within eight seconds of each other on May 22, 2026:
js-mirror[.]com - 13:40:28 UTC
npm-cache[.]com - 13:40:32 UTC
pypi-get[.]com - 13:40:36 UTC
All three use the same nameservers.
Fourteen minutes and 23 seconds after the last registration, FixedFloat transferred 0.01805723 ETH to the operator's wallet ( 0x55F9780e…f31cD ).
Three days later, on May 25, the wallet deployed the Ethereum resolver contract, wrote all three domains into it, and 2 hours and 35 minutes after that narrowed the list to just npm-cache[.]com . The next morning it transferred 0.00436 ETH to a Binance-labeled deposit address. The accounting reconciles to the wei.
A timeline showing the deployment of the campaign infrastructure is shown below in Figure 7.
FixedFloat is a shared exchange wallet with millions of transactions. This wallet tells us the funding rail, not the operator's identity. The Binance deposit address is the strongest identity pivot.
On Aug. 4, 2026, the attacker executed an on-chain transaction 0xc55920f1bd0531b6738153068a666c080ddded47e6256f1fd980d51c0b507c91 to modify the StringListStore in smart contract 0xE1f2395ee43e45A1556EC6438a88c31B83493103 , rotating the active C2 domain from npm-cache[.]com to a newly registered domain, awqhnjewqjkl[.]icu . The transaction was submitted by wallet 0x55F9780ef31cD , the same wallet that originally deployed the C2 smart contract on May 25, 2026.
The new domain awqhnjewqjkl[.]icu was registered via NameSilo, LLC at 15:15:26 UTC on Aug. 4, 2026, and was operationally active within the hour as the earliest observed connection observed by Unit 42 researchers occurred at 16:10:03 UTC.
The domain exhibits characteristics consistent with domain generation algorithm (DGA) output: a randomized 12-character string on the .icu top-level domain (TLD), flagged as DGA by the VirusTotal community. This represents a shift from the previous C2 domain npm-cache.com , which used a naming convention that mimicked a developer ecosystem and was registered through a different registrar (Tucows/OpenSRS).
Despite the change in registrar and naming convention, both awqhnjewqjkl[.]icu and npm-cache[.]com are proxied through Cloudflare's cloud delivery network (CDN) infrastructure. Both domains serve the identical Cloudflare default CDN-CGI stylesheet d30b4ea6f68456672f5abb35e9dcf7d54226372b66e9d60a7ee26b7a52568e74 , confirming shared use of the Cloudflare proxy layer.
The new domain was iss
…(truncated for reading performance)
Discussion
Sign in to join the discussion.