ChainDrop keyv / cacheable npm worm
Summary
On August 4, 2026, StepSecurity, Socket, and Aikido reported 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 described it as active 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. Scope and execution evidence reflect StepSecurity's 18:10 UTC revision on August 4. Counts and registry state can change; use the linked vendor-maintained lists for live scoping.
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.
Confidence and attribution
- The compromise and malicious package behavior are corroborated by StepSecurity, Socket, and Aikido.
- 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.
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 / @file-entry-cache |
11.1.6 as reported; verify registry identity against vendor lists |
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.
Indicators and hunting pivots
Files and execution
setup.mjsMath_Symbol.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 ~/.local/bin/gh-token-monitor.shand unexpected associated user-level systemd service or macOS LaunchAgent- temporary files matching
tmp.dpkg_<pid>.lock - 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
SHA-256
fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb—setup.mjs54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668—setup.mjstarball variant9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc—math_init.js/Math_Symbol.js
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 /routertraffic
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.
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.
- 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; StepSecurity's 18:10 snapshot is 444 packages and 2,212 versions.
- 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 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