ChainDrop keyv / cacheable npm worm
Summary
Beginning August 4, 2026, StepSecurity, Socket, Aikido, Wiz, Snyk, JFrog, SafeDep, Elastic Security Labs, and Unit 42 reported on a fast-moving npm supply-chain worm affecting keyv, the cacheable package family, and packages reachable through stolen maintainer identities. StepSecurity named the activity ChainDrop. Aikido, JFrog, and Elastic described it as Shai-Hulud activity; Socket assessed that its tradecraft closely matches Shai-Hulud but did not recover a self-identifying campaign marker from the analyzed payload.
The malicious releases add an npm preinstall hook, download Bun 1.3.13, run a heavily obfuscated second stage, harvest developer, CI/CD, cloud, package-registry, Vault, and Kubernetes credentials, and use stolen npm access or OIDC trusted publishing to republish trojanized packages. Socket also reported GitHub and DNS exfiltration plus .claude and .vscode repository hooks that can execute when source is opened without requiring npm install.
This is an active incident. SafeDep's later August 4 snapshot counted 2,234 poisoned versions across 444 package names and 12 organizations, 22 more versions than StepSecurity's 18:10 UTC snapshot while leaving the package-name count unchanged. Unit 42's August 9 response-list update separately names 483 packages and 1,675 package-version pairs. The inventories use different collection and classification windows; use their union for scoping rather than treating either count as final.
Tags
- ops
- operations
- supply-chain
- npm
- credential-theft
- worm
- Shai-Hulud
- ChainDrop
- Bun
- GitHub Actions
- OIDC
- CI/CD
- developer-targeting
- Ethereum
- EtherHiding
- remote-access
- persistence
Why this matters
- StepSecurity's 18:10 UTC revision reported 444 package names and 2,212 compromised versions observed between 09:40 and 13:20 UTC. Aikido separately reported at least 1,280 compromised packages in an earlier update. The difference reflects changing collection windows and package classification during an active incident; neither count should be treated as final.
- StepSecurity separated 11 full worm carriers in the Jared Wray ecosystem from 433 propagated package names covering 2,201 versions. This distinction matters: the first group carried the complete propagation logic, while the second wave was republished through credentials harvested from at least a dozen unrelated organizations.
- The initial package family sits deep in common dependency trees. Public reporting identifies
keyv,cacheable-request,flat-cache,file-entry-cache, and related caching packages used transitively by developer tooling. - Aikido says malicious source changes were pushed to the legitimate repository and released through GitHub Actions, so affected packages could carry valid provenance. Provenance proved which workflow built the artifact, not that the source or maintainer identity was clean.
- The payload turns credential theft into automated package propagation and adds source-repository execution paths for IDEs and AI coding agents.
- By StepSecurity's 18:10 UTC update, npm had reverted all 11 full worm carriers to safe versions. Cleanup of the propagated wave was incomplete:
@servicetitan/*and@nebula.js/*removals were underway, clean replacements existed for@thiennq/docs-viewerand@onereach/ui-components, and two reported malicious releases still held thelatesttag. Registry cleanup does not remove copies already pinned in lockfiles, mirrors, caches, or artifacts. - StepSecurity found real execution in ten public
backstage/backstageCI runs between 09:31 and 10:40 UTC. Fresh E2E scaffolding resolved a compromised transitive dependency outside the repository's committed lockfile; Bun then contacted Ethereum RPC services andnpm-cache.com. StepSecurity found no evidence of long-lived credential loss in those runs because the affected workflows referenced no repository secrets, but the payload did execute and reach C2. - Snyk independently fetched and compared the maintainer-linked tarballs without installing them, confirmed the same 11 full-carrier releases, and published malicious-code advisory
SNYK-JS-KEYV-18515941forkeyv@6.0.0. Its registry sweep also found that the other@keyv/*packages published before the payload commit did not carry the malicious lifecycle hook, preventing an overbroad all-@keyvassessment. - SafeDep's later registry reconstruction counted 2,234 poisoned versions under the same 444 package names across 12 organizations between 09:35 and 13:18 UTC. It found that 80% of affected names carried more than one poisoned release and 43 names carried at least 11, making package-name, lifecycle-hook, and payload-hash checks more durable than relying on an early version list.
- SafeDep found the same 727,680-byte payload across the initial
keyv/cacheable,@hubsync, and@ornikarpublisher clusters, but two loader builds and different publication paths. The initial family retained valid OIDC/SLSA provenance, while the latter clusters used direct npm identities without provenance. One campaign therefore crossed both source/CI compromise and stolen-token publication paths. - JFrog independently recovered repository-infection and GitHub Actions secret-harvesting detail, including branch and workflow artifacts plus file hashes defenders can hunt independently of the package list.
- Wiz's 19:50 UTC payload update found selective, C2-controlled arming of the token-revocation dead-man switch, per-host fingerprinting, a rotated exfiltration key, nearly 70% more credential-target definitions, and two prior smart-contract-resolved domains. This changes containment and scoping: responders should not assume every infected host received the same destructive command or that blocking only the currently resolved domain covers the campaign history.
- Microsoft Threat Intelligence independently classified the payload as a Mini Shai-Hulud variant and published Defender detections and Advanced Hunting pivots. Microsoft also found that many propagated patch releases had no matching source commit, pull request, tag, or legitimate release, supporting direct tarball modification with stolen npm publication access rather than a separate source-repository compromise for every affected publisher.
- Sonatype independently tracked 2,225 affected component versions under
sonatype-2026-005579. Its August 5 snapshot is nine versions below SafeDep's later 2,234-version reconstruction, reinforcing that vendor inventories reflect different collection and classification windows rather than a stable final count. Sonatype also addedMath_Init.jsas a filename/case variant and explicitly recommends treating any environment that executed an affected version as potentially compromised. - Elastic Security Labs independently detonated the payload and observed an additional smart-contract-resolved C2 domain,
awqhnjewqjkl[.]icu, alongsidenpm-cache[.]com. Elastic also published endpoint process and DNS hunts and identified worm-generated Git commits by the authorclaude/claude@users.noreply.github.comand messagechore: update config. - Unit 42 detected ChainDrop execution in 10 distinct environments and found 453 public repositories across five accounts matching the worm's exfiltration marker and Dune-themed naming pattern. It treats those accounts as candidate, not confirmed, victims; the repositories had been removed by publication time.
- Unit 42 tied
awqhnjewqjkl[.]icuto a specific August 4 Ethereum transaction and observed the rotated domain become operational within an hour, followed by traffic involving environments on four continents. This converts the domain from a detonation-only pivot into an observed live C2 rotation while still not establishing how many connections represented successful compromise. - On August 9, Unit 42 updated its article with a public response list containing 483 unique package names and 1,675 listed package-version pairs. That is 39 more package names but 559 fewer version pairs than SafeDep's 444-name / 2,234-version snapshot. The difference is a scoping signal, not evidence that one list supersedes the other: preserve both lists, search their union across lockfiles, caches, mirrors, SBOMs, and built artifacts, and retain the source and retrieval time with results.
- OX Security reported a new distribution boundary on August 9: a ChainDrop-infected repository was linked from a V.A.P.E entry in the public MCP Registry, while its referenced PyPI package remained clean. The repository carried the already known
.claude/settings.jsonand.vscode/tasks.jsonexecution hooks, showing that registry metadata can route users to a poisoned source tree even when package-only scanning finds nothing. - OX also found five GitHub repositories still serving the hook pair five days after the initial wave and said two
@ornikarpackages remained available for roughly 72 hours. A threat.wiki recheck early on August 10 found the exact known hook hashes in four of the five named repositories; the fifth repository had changed, and the MCP Registry API no longer returned V.A.P.E. Treat repository and registry status as volatile response snapshots, not permanent indicators.
Confidence and attribution
- The compromise and malicious package behavior are corroborated by StepSecurity, Socket, Aikido, Wiz, Snyk, JFrog, SafeDep, Microsoft Threat Intelligence, Sonatype Research Labs, Elastic Security Labs, and Unit 42.
- Aikido labels the wave active Shai-Hulud activity. Socket says the behavior closely matches Shai-Hulud: TruffleHog-style secret collection, maintainer-package enumeration, npm token and OIDC publication, and victim-account GitHub repositories.
- Socket did not recover the campaign's self-identifying repository or commit markers because relevant strings were assembled at runtime. Public Shai-Hulud-derived tooling also makes copycat reuse possible. Track ChainDrop as a Shai-Hulud-lineage assessment, not confirmed TeamPCP attribution.
- StepSecurity assesses the payload as a direct, heavily evolved descendant of Shai-Hulud 2.0 based on Bun/preinstall delivery,
Runner.Workermemory scraping, npm self-republication, and GitHub exfiltration. Its Russian-locale kill switch is an operator-language clue, not sufficient actor or nationality attribution. - Socket and Aikido identify compromise of the
Jaredwraymaintainer/GitHub account as the initial high-impact access path. Maintainer and registry postmortems were not yet public at capture time. - JFrog treats the
Shai-Hulud: Here We Go Againdead-drop description as a self-identifying campaign marker. That supports Shai-Hulud lineage, but a reusable public marker still does not establish TeamPCP operator identity. - Microsoft Threat Intelligence calls the payload a Mini Shai-Hulud variant. This strengthens multi-vendor lineage agreement but does not resolve whether TeamPCP, another operator, or a copycat controlled this incident.
- Unit 42 likewise concludes that the code is clearly in the Shai-Hulud lineage but says the public May 2026 source release prevents confident TeamPCP attribution.
Reported execution chain
- The attacker publishes a new package version containing
setup.mjs,Math_Symbol.js(also referenced internally asmath_init.js), and"preinstall": "node setup.mjs". setup.mjsdetects platform and architecture, including Alpine/musl, and obtains a platform-specific Bun1.3.13runtime from the legitimateoven-sh/bunGitHub Releases path when Bun is absent.- The loader extracts Bun with system
unzip, PowerShellExpand-Archive, or a JavaScript ZIP fallback, then executes the second stage under Bun. - Socket describes the second stage as a roughly 728 KB bundle with polymorphic basE91-protected strings and internal modules tagged
[collector],[dispatcher],[provenance], and[publish]. - The collector reads local credentials, environment variables, cloud metadata, managed secret stores, runner identity material, and generic token/private-key patterns.
- The worm calls npm identity and search endpoints, discovers packages reachable by the stolen maintainer identity, downloads clean tarballs, injects its files and lifecycle hook, bumps versions, recomputes integrity metadata, and republishes.
- Where trusted publishing is available, it attempts npm's OIDC token-exchange endpoint. A poisoned source tree can therefore produce a valid npm/Sigstore provenance attestation.
- Socket reports encrypted findings sent through attacker-created GitHub repositories and a separate DNS channel. StepSecurity further observed
results-*.jsonstaging repositories and a GitHub-token monitor that creates a delayed execution path when defenders revoke the stolen token. - The source repository can receive
.claude/settings.jsonSessionStarthooks and.vscode/tasks.jsonfolderOpentasks that rerun the loader when a developer or coding agent opens a clone. - StepSecurity reports that the payload resolves command-and-control domains from an Ethereum mainnet contract, falls back to signed-commit searches on GitHub, and sends encrypted data to
/router. A response containing acodefield is passed toeval, making the channel bidirectional remote access rather than exfiltration only.
Credential and secret targets
Reported collection includes:
- npm authentication tokens and registry identity/token endpoints;
- GitHub CLI tokens, PATs, session material, Actions OIDC request tokens, and organization/repository secrets;
- AWS credential chains, instance/container metadata, and Secrets Manager across regions;
- GCP service-account private keys and Azure client secrets;
- HashiCorp Vault tokens, including
/home/runner/.vault-token,/run/secrets/VAULT_TOKEN, andVAULT_TOKEN; - Kubernetes service-account tokens under
/var/run/secrets/kubernetes.io/serviceaccount/token; - generic bearer tokens, API keys, and private-key blocks discovered with a TruffleHog-style filesystem sweep.
Initial high-impact package set
Socket's ongoing list at capture time included:
| Package | Reported malicious version |
|---|---|
keyv |
6.0.0 |
cacheable |
2.5.1 |
cacheable-request |
13.0.20 |
flat-cache |
6.1.24 |
@cacheable/net |
2.1.1 |
@cacheable/node-cache |
3.1.2 |
@cacheable/memory |
2.2.1 |
@cacheable/utils |
2.5.1 |
cache-manager |
7.2.10 |
file-entry-cache |
11.1.6 |
StepSecurity's early list also included packages in @arv-bedrock, @deliveroo, @hubsync, @onereach, @or-sdk, @ornikar, @picsart, @qlik, and @servicetitan scopes, plus ecto, pob-test-typescript-package-in-monorepo, and tslint-folder-schema. Aikido reported rapid spread into additional maintainers and organizations. Do not treat this table as complete.
StepSecurity scope update — 13:20 UTC
StepSecurity's updated incident snapshot counted 444 package names and 2,212 malicious versions during the 09:40–13:20 UTC observation window. Eleven Jared Wray ecosystem packages were full carriers; the worm republished 433 additional package names and 2,201 versions through credentials belonging to at least a dozen unrelated organizations. Many historical versions were replayed, so the version count is not a victim or execution count. Scope exposure from lockfiles and caches separately from confirmed execution of the preinstall hook.
StepSecurity technical update — 15:13 UTC
The expanded analysis places the first poisoned keyv commit (ee2681a) at 09:02:37 UTC and the repository-hook commit (d8c850c) at 09:04:30. keyv@6.0.0 was then published at 09:35 through GitHub Actions workflow run 30896232272 with a valid trusted-publishing attestation. StepSecurity observed the automated second wave beginning at 09:38:13 and continuing through 11:44.
The payload also:
- exits when
LANGindicates a Russian locale and otherwise respawns detached outside GitHub Actions; - writes a camouflaged
<tmpdir>/tmp.dpkg_<pid>.lockstate file; - creates
results-*.jsonexfiltration commits under victim identities; - installs
~/.local/bin/gh-token-monitor.shwith a user service or macOS LaunchAgent, pollsapi.github.com/userevery 60 seconds for 24 hours, and executes an attacker-supplied handler after token revocation; - resolves C2 through Ethereum and sends a gzip, AES-256-GCM, RSA-OAEP-SHA256, and base64 envelope that StepSecurity says it intercepted and decrypted in its sandbox.
StepSecurity containment update — 18:10 UTC
StepSecurity reported that npm's rolling response began with removal of cacheable-request@13.0.20 at 10:39 UTC and a keyv dist-tag rollback to 5.6.0 around 11:15. By 18:10, all 11 full carriers had been reverted to safe versions. The response was still incomplete across the worm-propagated package set:
@servicetitan/*and@nebula.js/*packages were being removed wholesale;- clean releases were available for
@thiennq/docs-viewer@1.6.4and@onereach/ui-components@27.0.4; @picsart/ai-sdk@3.32.2and@deliveroo/reevent@1.0.1reportedly remained onlatestat the capture time; and- the compromised maintainer account and three initially affected GitHub repositories were no longer available, limiting access to the original issue and commit history.
Treat these as a time-bounded response snapshot, not a final registry inventory. Continue using vendor-maintained affected-version lists and inspect internal registry proxies, package caches, lockfiles, and built artifacts even after public removal or dist-tag rollback.
Confirmed public CI execution — Backstage
StepSecurity searched roughly 44,000 public workflow runs from an eight-hour window for connections to npm-cache.com. Excluding five runs in its own detonation repository, it found ten matching runs in backstage/backstage. In the affected E2E jobs, fresh application scaffolding installed current dependencies outside the project's committed lockfile. The resulting Bun process contacted eth.llamarpc.com, go.getblock.io, eth-mainnet.nodereal.io, and then npm-cache.com in the same sequence observed in StepSecurity's detonation.
The runs occurred across Renovate pull requests, pushes to master, and changeset branches between 09:31 and 10:40 UTC. StepSecurity reported the finding to Backstage as issue backstage/backstage#35100. It found no evidence of long-lived credential loss: the two affected workflow definitions referenced no repository secrets, the only job credential was an ephemeral GITHUB_TOKEN, and the older audit-mode agent did not record a Runner.Worker memory-read event. This is confirmed payload execution and C2 reachability, not confirmed credential theft.
The case demonstrates a lockfile boundary: a committed application lockfile does not constrain jobs that scaffold a new project or otherwise resolve fresh dependencies during testing.
Snyk independent tarball validation
Snyk queried npm for 61 package names associated with maintainer jaredwray, checked August 4 releases, and independently confirmed the 11 full-carrier versions listed above. It found ecto@5.0.1 particularly important for scoping because it appeared after the first public warnings, while the other @keyv/* version 6 packages published between 09:30 and 09:32 predated the payload and did not contain the malicious hook.
Its file-by-file comparison of keyv@6.0.0 against keyv@6.0.0-rc.1 found the compiled dist/ tree unchanged. The material differences were the stable version metadata, setup.mjs, Math_Symbol.js, and the preinstall hook. Snyk calculated SHA-256 d584f9b6af48b7ed1f93713944f033783bf149e1c25e1643eb8c0e9df5dc7782 for the keyv@6.0.0 tarball and independently reproduced the two payload hashes below.
Snyk also clarified the commit-verification boundary. The initial poisoned release commits were reported as unsigned, while repository-hook commit d8c850c7 was GitHub-verified and used github-actions[bot] identity. A verified commit badge proves how GitHub signed that commit object; it does not prove the project authorized the change. Snyk did not execute or independently decrypt the second stage, so detailed second-stage capabilities remain grounded in the runtime and malware analyses cited above rather than in Snyk's static validation.
JFrog and SafeDep scope and infrastructure follow-up
SafeDep reconstructed 2,234 poisoned versions across 444 package names and 12 unrelated organizations between 09:35 and 13:18 UTC. This is 22 versions above StepSecurity's 18:10 snapshot but does not increase the package-name count. SafeDep reported 537 poisoned versions across 47 @ornikar/* and related names, 27 consecutive poisoned @hubsync/web-sdk-react releases, and continued republishing after initial public warnings. These are registry-observation counts, not confirmed installations or victims.
The same 727,680-byte stage-two payload, SHA-256 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc, appeared under different filenames across three publisher clusters. SafeDep separated two loaders: the 29,918-byte 54dc7ea5… build in the initial keyv/cacheable family and the 11,017-byte fd3ca400… build in @hubsync, @ornikar, and the repository's IDE hooks. The initial family published through a valid trusted-publisher workflow; the other two clusters lacked provenance and used direct npm identities. This demonstrates that one payload family can arrive through both compromised source/CI and stolen registry tokens.
SafeDep also verified that the earlier @keyv/* storage-adapter releases were clean as published, agreeing with Snyk's narrower scoping. It warned that a later repository commit staged the malicious files across 19 workspaces even though only core keyv had shipped the hook, so responders should distinguish published tarballs from compromised source awaiting a possible release.
Registry state remained uneven. SafeDep reported the initial keyv/cacheable family largely unpublished or rolled back, while many @hubsync and @ornikar names still resolved latest to poisoned releases. Its statement that there was “no C2 host” is narrower than, and conflicts with, StepSecurity and JFrog's dynamic-HTTPS analysis: JFrog independently documented the Ethereum contract, selector, and /router path. Treat SafeDep's finding as no plaintext embedded host in its analyzed material, not evidence that the Ethereum-resolved HTTPS channel was absent.
JFrog added hashes for the planted repository and workflow artifacts and reported that the worm can target up to 50 writable branches per repository, skipping dependabot/* and copilot/*. It described a separate dependabot/github_actions/format/setup-formatter branch containing .github/workflows/codeql_analysis.yml, which writes the secrets context to format-results.txt and uploads it as an artifact. These artifacts should be hunted even where package-install evidence is absent.
Wiz payload-control and credential-scope update
Wiz's August 4 update provides a later payload snapshot rather than another registry count. It reported that repository-hook commits had shifted to the message chore: update config, the state-file name had become tmp.dpkg_14527.lock, and the operator had rotated the RSA key used to encrypt exfiltrated data while retaining the key used to verify GitHub-hosted fallback C2 instructions.
The dead-man switch was no longer simply armed for every infection. Wiz found that the C2 code response could select both the command and the GitHub-valid token to monitor through api.github.com/user. The payload also attached a SHA-256 host identifier built from concatenated system properties to stolen data. Wiz assesses that this supports per-host delivery of customized dead-man-switch values. Responders should still isolate and stop the implant before token revocation, but should preserve C2 responses and host identifiers to determine which systems were actually armed.
Wiz independently confirmed Ethereum eth_call resolution through a StringListStore contract and reported that its on-chain history initially returned three domains before being changed to only npm-cache[.]com. Its IOC table identifies the two historical alternatives as pypi-get[.]com and js-mirror[.]com. The contract owner's funding source had previously been flagged for scam association; that is infrastructure context, not actor attribution.
The same payload revision expanded credential-target definitions by almost 70%, according to Wiz. Newly covered stores included Claude, OpenAI, Codex, Cursor, and Gemini agent credentials; Foundry, Solana, and Monero keystores; Jenkins master.key, Argo CD, and Harbor secrets; Alibaba Cloud and Tencent Cloud CLI configurations; and /etc/shadow. This widens post-execution rotation and review beyond the cloud, GitHub, npm, Vault, and Kubernetes targets in earlier reports.
Microsoft Defender and direct-publication follow-up
Microsoft Threat Intelligence's late August 4 analysis independently reproduced the preinstall-to-Bun execution chain, cloud and secret-store API enumeration, npm token and GitHub Actions OIDC propagation, Ethereum and signed-commit C2 resolution, GitHub fallback exfiltration, and repository-hook persistence. It classified the payload as a Mini Shai-Hulud variant.
Microsoft observed that many malicious patch releases lacked a corresponding source commit, pull request, tag, or legitimate release. This supports a mixed propagation model: the initial keyv path could publish attacker-controlled source through a legitimate OIDC workflow, while many later publisher identities were used to alter and publish tarballs directly. A clean public repository is therefore not evidence that its npm artifact was clean.
Microsoft added two useful implementation details:
- the repository path set includes
.claude/setup.mjsand.vscode/setup.mjsalongside the previously reported settings and task files; and - one GitHub fallback path stores a stolen token under double Base64 encoding rather than the RSA/AES encrypted results envelope. Responders should not assume every value in a
results-*.jsonrepository is cryptographically protected or search only for encrypted blobs.
Published Microsoft Defender Antivirus labels include Trojan:NPM/ShaiLoader.BY, Trojan:NPM/MalBun.A, and Trojan:NPM/ShaiWorm.DAY!MTB, with behavior detections Behavior:Linux/SuspBunActivity.A and Behavior:Win32/SuspBunActivity.A. Microsoft Defender for Endpoint hunting focuses on node setup.mjs, a Node-launched bun or bun.exe under bun-dl-* or node_modules, the three known loader/payload hashes, and Bun-launched credential commands such as gh auth token, gcloud config config-helper, az account get-access-token, and azd auth token.
Sonatype affected-version and response update
Sonatype Research Labs' August 5 snapshot tracks 2,225 component versions under advisory sonatype-2026-005579. That total is lower than SafeDep's 2,234-version reconstruction despite being published later. Treat the difference as a reminder that active-incident inventories depend on observation windows, registry state, and classification rules; do not infer that nine versions were remediated or removed solely from the count change.
Sonatype independently describes the same preinstall → setup.mjs → downloaded Bun → Math_Symbol.js / Math_Init.js chain, credential discovery across files, environment variables, processes, metadata services, Kubernetes, and Vault, and automated republishing through stolen npm access. It also highlights the case-sensitive Math_Init.js spelling in addition to names already reported by other vendors.
For response, Sonatype says an environment that installed an affected release should be treated as potentially compromised. Its guidance aligns with the containment order already recorded here: isolate and preserve evidence, remove malware and persistence, then revoke and rotate exposed credentials, and rebuild from known-good components. A numerically later package release is not sufficient evidence of cleanliness.
Elastic endpoint and C2 follow-up
Elastic Security Labs' August 6 analysis independently reproduced the npm preinstall path, Bun download, cross-platform payload, credential collection, automated npm republishing, Ethereum dead-drop resolution, GitHub fallback, and Claude Code / VS Code repository hooks. Its detonation observed awqhnjewqjkl[.]icu returned through the smart-contract discovery path in addition to npm-cache[.]com. This is a new historical C2 pivot; because the contract is mutable, a domain's presence in historical telemetry is stronger evidence when correlated with Bun or Node ancestry, the contract query, or known files.
Elastic reported that the collector checks more than 300 credential patterns and highlighted AI-tool credentials for Anthropic, Claude, Codex, Cursor, OpenAI, and Gemini alongside cloud, GitHub, npm, Vault, SSH, and Kubernetes material. It also tied worm-generated repository changes to author claude / claude@users.noreply.github.com and commit message chore: update config. The author and message are hunt pivots, not proof on their own: both fields can be chosen by legitimate users or copied by another actor.
Elastic's process hunts focus on node setup.mjs, Bun command lines containing Math_Symbol.js or math_init.js, and Bun/Node DNS lookups for the Ethereum RPC providers plus the two observed C2 domains. Its reported 1.3-billion-plus aggregate monthly-download figure describes package reach, not malicious-version downloads, successful installations, or victims.
Unit 42 observed execution and C2-rotation follow-up
Unit 42's August 6 deobfuscation adds victim-side and control-plane evidence. It detected ChainDrop execution in 10 distinct environments and, at approximately 12:20 UTC on August 4, found 453 public GitHub repositories across five accounts matching the exact Shai-Hulud: Here We Go Again description and the worm's Dune-themed repository-name generator. The repositories were later removed. Unit 42 explicitly treats the five accounts as candidate rather than confirmed victims; repository creation proves attacker-pattern activity under those identities, not the initial infection path or downstream impact.
The report also identifies the exact Ethereum state-change transaction 0xc55920f1bd0531b6738153068a666c080ddded47e6256f1fd980d51c0b507c91. The contract owner rotated the active domain from npm-cache[.]com to awqhnjewqjkl[.]icu on August 4. Unit 42 says the new domain was registered at 15:15:26 UTC, first observed in use at 16:10:03 UTC, and reached by environments in North America, Europe, Asia, and Africa within roughly 19 hours. The reported addresses 104.21.91[.]101 and 172.67.215[.]154 are shared Cloudflare edge addresses; hunt or block on domain/SNI and correlated process ancestry rather than treating the IPs as attacker-owned infrastructure. Monitoring contract setter 0xd3c159e5 can provide warning of future rotations.
Unit 42 recovered a narrowly gated propagation branch for opensearch-project/opensearch-js. When GITHUB_ACTIONS is set and GITHUB_REPOSITORY and GITHUB_WORKFLOW_REF match the repository and release-drafter.yml, the code requests npm and Sigstore OIDC tokens, injects a pinned @opensearch/setup dependency into @opensearch-project/opensearch, and can publish with a genuine Fulcio/Rekor-backed SLSA statement. Unit 42 did not observe this branch execute. Treat it as implemented, reachable targeting evidence—not proof that OpenSearch was compromised or that the typosquatted dependency was published.
Its sample also narrows persistence claims. The VS Code task calls .claude/setup.mjs and reaches the dropped .claude/math_init.js; the reciprocal Claude Code hook calls .vscode/setup.mjs, but that loader looks for a missing .vscode/math_init.js, so only the VS Code path was functional in that build. The macOS LaunchAgent and Linux user-service installer was embedded but had no call site in the analyzed main path. Other reports observed or described token-monitor persistence, so responders should still hunt all artifacts while distinguishing latent code from execution evidence on each host.
OX MCP Registry and residual-repository follow-up
OX Security's August 9 response review found a V.A.P.E MCP server entry whose linked PyPI package was clean but whose source repository, jUXTAPOSITION1/V.A.P.E, contained the ChainDrop .claude/settings.json and .vscode/tasks.json hooks. The Claude SessionStart hook called node .vscode/setup.mjs; the VS Code folderOpen task called node .claude/setup.mjs. Their SHA-256 values match the JFrog-published hook indicators already listed below. This is a registry-to-repository delivery path, not evidence that the clean PyPI artifact itself was compromised.
OX named five repositories that were still distributing this configuration pair at publication time:
techtoboggan/claude-desktop-hardened-linuxrainb0w-clwn/node-cache-manager-fs-binary-tsdiegobbarbosa09/Automacao_swaglabs_cypressevilgodfahim/kaljUXTAPOSITION1/V.A.P.E
An August 10 threat.wiki API recheck found all five repositories reachable, but only the latter four still exposed both known hook files. The V.A.P.E search returned no result from the MCP Registry API. Those changes reinforce that public takedown state is transient: retain repository IDs, commit and blob metadata, audit logs, clones, and endpoint evidence rather than relying on a later URL check.
OX separately reported that @ornikar/intl-config and @ornikar/react-native-svg-transformer remained in npm for about 72 hours after the initial infection, and estimated more than 3,800 searchable results-<ID>.json credential-dump repositories across recent Shai-Hulud variants. The latter is a cross-variant structural count and should not replace Unit 42's narrower ChainDrop finding of 453 repositories across five candidate victim accounts. Repository-pattern matches also do not independently establish credential validity, unique victims, or downstream use.
August 9 affected-package inventory update
Unit 42 marked the article updated at 16:06:20 UTC on August 9 and linked a public GitHub response list. The file contains 483 unique package rows and 1,675 comma-delimited package-version pairs. It includes scoped families such as @servicetitan, @onereach, @or-sdk, @ornikar, @qlik, @nebula.js, @redhat-cloud-services, and the initial @cacheable family, plus unscoped packages.
This list expands package-name scoping beyond SafeDep's 444-name reconstruction but contains fewer version pairs than SafeDep's 2,234. Public reporting does not yet reconcile the methodology, collection time, or inclusion rules behind that difference. Responders should not subtract entries found in only one source. Store a local, timestamped copy of each vendor list and search the union; public lists can change while compromised artifacts remain in internal mirrors and caches.
Unit 42 SDLC-perspective follow-up (August 21)
Unit 42's August 21, 2026 "Connecting the Dots" post reframes ChainDrop (and the Shai-Hulud lineage) as evidence that supply-chain attack focus has shifted from hunting bugs in finished software to poisoning the digital factory that builds it — CI/CD pipelines, developer endpoints, and registries. No new ChainDrop indicators are reported; the post's durable additions are scoping and defender heuristics:
- Three-step ChainDrop chain, restated for defenders: (1) the hook — malicious
preinstallscripts download the legitimate Bun runtime to silently launch the ~727 KB obfuscated payload; (2) the theft — a hidden Python script reads live process memory from GitHub Actions runners to steal temporary OIDC tokens and secrets (not just disk files), plus a local developer-credential sweep; (3) the payload — stolen npm/GitHub tokens republish infected packages with legitimate functionality intact, with long-term persistence via cross-linked hooks in VS Code and Claude Code and C2 managed through Ethereum transactions. - Runner-memory scraping is the distinguishing primitive. ChainDrop's value over disk-only scrapers is that build-runner OIDC tokens and CI secrets exist transiently in process memory; defenders who hunt only file-based credential theft miss this. Correlate
npm install/preinstallancestry with Python memory-access behavior on CI runners. - SBOM-at-the-end-of-build is insufficient. The post's core claim: an SBOM inventory created at the finish line does not catch malware that executed during the build. Map every place third-party packages touch (developer laptops, CI/CD, cloud) and treat build-time execution as the incident class, not build-time presence only.
- Developer endpoints lack the guardrails browsers have.
npm install,pip install,cargo build, IDE extensions, and setup scripts run with the user's full permissions and no sandbox; registry and marketplace targets (cf. the earlier GlassWorm developer-botnet activity) are prime because of that, not in spite of it. - Named historical anchors: XZ Utils CVE-2024-3094 (years-long contributor impersonation to hide backdoors in core software), the Axios supply-chain attack (account hijack to drop malware into popular libraries), the Shai-Hulud npm worm (setup-script credential theft), and the GlassWorm developer supply-chain botnet.
Indicators and hunting pivots
Files and execution
setup.mjsMath_Symbol.jsMath_Init.jsmath_init.js- npm lifecycle entry
"preinstall": "node setup.mjs" - process chain
node setup.mjsspawning a downloadedbun - temporary paths matching
bun-dl-* - unexpected
.claude/settings.jsonSessionStarthooks - unexpected
.vscode/tasks.jsontasks withrunOn: folderOpen - unexpected
.claude/setup.mjsor.vscode/setup.mjsrepository loaders ~/.local/bin/gh-token-monitor.shand unexpected associated user-level systemd service or macOS LaunchAgent- temporary files matching
tmp.dpkg_<pid>.lock tmp.dpkg_14527.lockin the later Wiz-analyzed payload revision- victim-account repositories or commits containing
results-*.json ~/.config/gh-token-monitor/,~/.config/systemd/user/gh-token-monitor.service, or~/Library/LaunchAgents/com.user.gh-token-monitor.plist- unexpected GitHub Actions workflow named
Run Copilot, artifact namedformat-results, or workflow content that writes${{ toJSON(secrets) }}toformat-results.txt - branch
dependabot/github_actions/format/setup-formatter, workflow.github/workflows/codeql_analysis.yml, outputformat-results.txt, commit messageAdd CodeQL Analysis, or forgedgithub-advanced-security[bot]identity - repository-hook commit message
chore: update config - repository-hook author
claudeorclaude@users.noreply.github.com, especially when paired withchore: update configand changes under.claude/or.vscode/ router_runtime.js- unexpected
@opensearch/setupdependency insertion oropensearch-project/opensearch-jsrelease-drafter.ymlOIDC publication activity; Unit 42 did not observe this targeted branch execute - repository commits
ee2681a9b62f3637b0eb5133c36c864d3376cc5b(payload),d8c850c7800e…(IDE/agent hooks),f97eabcdd057105f1fce3f05d6c029dac3f2ac78(evidence removal), and174f6a55690b0812a69adef47260ba8714a9be48(sibling staging)
SHA-256
d584f9b6af48b7ed1f93713944f033783bf149e1c25e1643eb8c0e9df5dc7782—keyv@6.0.0npm tarball, independently calculated by Snykfd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb—setup.mjs54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668—setup.mjstarball variant9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc—math_init.js/Math_Symbol.js927387d0cfac1118df4b383decc2ea6ba49c9d2f98b47098bcbcba1efc026e1f— planted.vscode/tasks.json, per JFrog14eb4ce01dd4307759887ff819359b70d7d9ff709ecde039a5abc1aac325b128— planted.claude/settings.json, per JFrog3f3f42d072bd36860ab7bd7fb5e10ac0d22c741c13c89505ccd6ec0ea572eea7— injected GitHub Actions workflow, per JFrog29ac906c8bd801dfe1cb39596197df49f80fff2270b3e7fbab52278c24e4f1a7— runner-memory scraper, per JFrogb27b82afa5f15512f3856e549fb83d873fd0049759a4b62ce64c8d7d4dc2c678— Unit 42 TLSH-pivotedsetup.mjs.maliciousvariant
Network and control-plane behavior
github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/— legitimate Bun distribution path; validate package-install process ancestry rather than blocking blindly169[.]254[.]169[.]254and169[.]254[.]170[.]2metadata access from developer or build processesregistry[.]npmjs[.]org/-/whoamiregistry[.]npmjs[.]org/-/npm/v1/tokensregistry[.]npmjs[.]org/-/npm/v1/oidc/token/exchange/package/- GitHub API
POST /user/repos, GraphQLcreateCommitOnBranch, newly created repositories, and commits from developer or CI identities - anomalous DNS exfiltration from package-install or Bun processes
- Ethereum mainnet contract
0xE1f2395ee43e45A1556EC6438a88c31B83493103, queried witheth_callselector0x53ed5143 - GitHub commit-search strings
thebeautifulmarchoftimeandIfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients npm-cache[.]com— C2 domain observed by StepSecurity; investigateGET /routerhealth checks returning HTTP 400/404 and encryptedPOST /routertrafficawqhnjewqjkl[.]icu— additional smart-contract-resolved C2 domain observed during Elastic Security Labs detonationpypi-get[.]comandjs-mirror[.]com— historical smart-contract-resolved domains reported by Wiz; validate current ownership and historical DNS/HTTP telemetry before blocking- Ethereum transaction
0xc55920f1bd0531b6738153068a666c080ddded47e6256f1fd980d51c0b507c91— Unit 42-observed August 4 rotation toawqhnjewqjkl[.]icu - contract owner
0x55f9780e1492344b7417fa723aedc4d0b97f31cdand setter selector0xd3c159e5— monitor as control-plane pivots, not actor identity 104.21.91[.]101and172.67.215[.]154— shared Cloudflare edges observed forawqhnjewqjkl[.]icu; do not block or attribute these IPs without hostname contextBun/1.3.13— Wiz-reported user agent; correlate with npm install, temporary Bun-download paths, and the listed C2/RPC destinations because Bun itself is legitimate
The npm and GitHub endpoints are legitimate. Alert on unusual process ancestry, identity, volume, and timing rather than treating the domains as stand-alone malicious indicators.
Defender actions
Immediate exposure check
- Freeze dependency updates and block the affected package families/scopes at registry proxies while the maintainer and registry response is unresolved.
- Search
package-lock.json,yarn.lock,pnpm-lock.yaml, package-manager caches, CI logs, SBOMs, and artifact repositories for the vendor-maintained package/version lists. - Determine whether install scripts executed. A lockfile reference without installation is a different exposure class from a developer host or runner that ran the malicious
preinstallhook. - Search cloned repositories independently for
.claudeand.vscodehooks because those paths can execute without npm installation.
If an affected version executed
- Isolate developer hosts and runners; stop active workflows and publication paths.
- Preserve endpoint, CI, npm, GitHub, DNS, cloud, and registry evidence before rebuilding.
- Before revoking the stolen GitHub token, contain the host and remove the token-monitor persistence described above; StepSecurity reports that token revocation can trigger an attacker-supplied handler. Then revoke and replace npm, GitHub, cloud, Vault, Kubernetes, SSH, CI, and application secrets; do not rotate only npm tokens.
- Audit npm for unexpected versions published by affected identities and GitHub for force pushes, deleted/recreated tags, new repositories, unexpected commits, transient workflows, OIDC exchanges, and repository hooks.
- Rebuild affected machines/runners and dependency caches from known-clean images and commits. Roll back to verified clean package versions only after maintainer or registry confirmation.
- Review cloud control planes for metadata-credential use outside expected hosts, broad secret enumeration, and activity by CI identities after the first package installation.
- Where Microsoft Defender telemetry is available, hunt for the published malware and behavior labels plus Node-to-Bun process lineage; preserve detections and process trees before reimaging.
Preventive controls
- Deny package install scripts by default where feasible and explicitly approve required scripts. This reduces the initial
preinstalllane but does not stop malicious source hooks or runtime imports. - Use dependency cooldowns and tarball diffs; flag new lifecycle hooks, large obfuscated root files, runtime downloads, and sudden releases across many packages.
- Microsoft specifically recommends npm CLI 11.10.0 or later and its
min-release-agecontrol. Treat this as a delay layer, not a substitute for install-script restrictions or artifact inspection. - Bind trusted publishing to protected GitHub Environments and branch rules. Provenance alone cannot distinguish a legitimate workflow building attacker-controlled source.
- Separate untrusted pull-request workflows from release permissions, protect release branches/tags, require reviewed changes to workflow and editor/agent configuration, and monitor force pushes.
- Restrict CI and developer egress to cloud metadata, secret stores, npm publication endpoints, GitHub repository creation, and unnecessary DNS resolvers.
Open questions
- Final affected package/version and download scope after npm containment; SafeDep's later August 4 snapshot is 444 packages and 2,234 versions across 12 organizations, while Unit 42's August 9 response list contains 483 package names and 1,675 package-version pairs.
- Initial access and whether the
Jaredwrayaccount, endpoint, token, GitHub session, or another upstream identity was first compromised. - Registry and GitHub containment actions, malicious-version removal times, and credential invalidation scope.
- Names, visibility, and recoverable indicators for attacker-created GitHub exfiltration repositories and the DNS channel.
- Current values and transaction history of the Ethereum C2 contract, replacement domains, and signed-commit fallback infrastructure.
- Whether additional package, MCP, skill, extension, or AI-tool registries still point to repositories carrying ChainDrop hooks despite clean linked package artifacts.
- Which infected hosts received selectively armed dead-man-switch instructions, what commands were returned, and whether the per-host identifier was used for other targeted tasking.
- Whether the implemented OpenSearch-specific trusted-publishing branch ever executed; current Unit 42 evidence does not show that it did.
- Whether ChainDrop is operated by TeamPCP, another Shai-Hulud-lineage actor, or a copycat using leaked tooling.
- Confirmed victim execution and downstream cloud/repository compromise beyond package publication.
- Final disposition of propagated releases, including the
@picsart,@deliveroo,@servicetitan, and@nebula.jsscopes, and whether clean restoration preserved or replaced package names.
Related pages
- Mini Shai-Hulud npm/PyPI worm campaign
- TeamPCP
- Developer-tool configuration auto-execution
- npm install explicit-trust controls
Sources
- StepSecurity: ChainDrop npm Worm: Bun-loaded CI/CD credential harvester with Ethereum dead-drop C2
- Wiz Research: keyv and cacheable npm Package Hijacked in Supply Chain Attack
- Snyk: Inside the keyv npm Compromise: preinstall Malware, Trusted Provenance, and IDE Hooks
- JFrog Security Research: Major Shai Hulud campaign strikes npm again, affecting keyv and 400+ packages
- SafeDep: npm Worm Poisons keyv, cacheable and 400+ Other Packages Across Twelve Organisations
- Socket: Popular npm Packages in the keyv and Cacheable Namespaces Compromised in Active Supply Chain Attack
- Aikido Security: Keyv and friends compromised in active Shai-Hulud supply chain attack
- Microsoft Security Blog: ChainDrop supply chain compromise: Anatomy of a self-propagating worm
- Sonatype Research: Mini Shai-Hulud npm Attack: More Than 2,200 Components Impacted
- Elastic Security Labs: Shai-Hulud strikes again: CHAINDROP worm hits 400+ npm packages
- Unit 42: ChainDrop: Inside a Self-Propagating npm Worm
- Unit 42 GitHub: List of packages affected by the ChainDrop worm
- OX Security: Shai-Hulud Outbreak Debrief: The Worm Evolves into MCP
- Unit 42: Connecting the Dots: Securing the Overlooked Corners of the Software Development Lifecycle (SDLC) Supply Chain (August 21, 2026)