Keyv and friends compromised in active Shai-Hulud supply chain attack

Keyv and friends compromised in active Shai-Hulud supply chain attack

Keyv 及相关库在“沙丘之虫”(Shai-Hulud)供应链攻击中被攻陷

On August 4, 2026, attackers compromised the GitHub account of the maintainer behind keyv, a key-value storage library with roughly 127 million weekly npm downloads, and used that access to inject a credential-stealing worm across the entire package family. The same maintainer owns cacheable (29M downloads/month), flat-cache (565M downloads/month), file-entry-cache (557M downloads/month), and several other widely-used caching utilities, all of which were swept up in the same attack.

2026 年 8 月 4 日,攻击者攻陷了 keyv 维护者的 GitHub 账号。keyv 是一个每周 npm 下载量约 1.27 亿次的键值存储库。攻击者利用该权限向整个软件包家族植入了一个窃取凭据的蠕虫病毒。同一位维护者还拥有 cacheable(每月 2900 万次下载)、flat-cache(每月 5.65 亿次下载)、file-entry-cache(每月 5.57 亿次下载)以及其他多个广泛使用的缓存工具,这些项目均在此次攻击中受到波及。

The compromise was carried out by pushing malicious files directly to the main branch and then immediately cutting a new release, meaning the poisoned versions were published to npm with valid provenance signed by GitHub Actions.

此次攻击是通过直接向主分支推送恶意文件并立即发布新版本来实施的,这意味着这些被投毒的版本在 npm 上发布时,带有由 GitHub Actions 签名的有效来源证明。

The compromised packages include: 受影响的软件包包括:

  • keyv 6.0.0 (604M/month)
  • flat-cache 6.1.24 (580M/month)
  • file-entry-cache 11.1.6 (571M/month)
  • cacheable-request 13.0.20 (137M/month)
  • cacheable 2.5.1 (30M/month)
  • @cacheable/memory 2.2.1 (28M/month)
  • cache-manager 7.2.10 (16M/month)
  • @cacheable/node-cache 3.1.2 (6M/month)
  • @cacheable/utils 2.5.1 (34M/month)
  • @cacheable/net 2.1.1 (3.7K/month)
  • ecto 5.0.1 (4.5K/month)

We are also seeing very active community spread of this supply chain worm to other maintainers and packages, including major organizations: @deliveroo/reevent 1.0.1, @or-sdk/invitations 1.4.9, @picsart/ai-sdk 3.32.2, @qlik/embed-runtime 1.6.4, picasso.js 2.11.6.

我们还观察到该供应链蠕虫正在社区中非常活跃地传播至其他维护者和软件包,包括一些大型组织的项目:@deliveroo/reevent 1.0.1、@or-sdk/invitations 1.4.9、@picsart/ai-sdk 3.32.2、@qlik/embed-runtime 1.6.4 以及 picasso.js 2.11.6。

Update — August 4, 2026, 13:37 CEST: At least 434 packages (across 1381 versions) have been compromised by the worm, with a combined total of over 2 billion monthly installs at the time of writing.

更新 — 2026 年 8 月 4 日,欧洲中部夏令时间 13:37:至少有 434 个软件包(涉及 1381 个版本)被该蠕虫攻陷,截至撰稿时,这些软件包的月安装总量超过 20 亿次。

What happened

事件经过

Every package in the family received two new files, setup.mjs and Math_Symbol.js, along with a "preinstall": "node setup.mjs" entry added to each package.json. Anyone who ran npm install against an affected version would have had setup.mjs execute automatically before their install completed.

该系列中的每个软件包都增加了两个新文件:setup.mjsMath_Symbol.js,并在每个 package.json 中添加了 "preinstall": "node setup.mjs" 条目。任何对受影响版本运行 npm install 的用户,都会在安装完成前自动执行 setup.mjs

setup.mjs is a heavily obfuscated dropper. Its only job is to silently download the Bun JavaScript runtime from github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/ and use it to execute the real payload, Math_Symbol.js: execFileSync(<bun binary>, ['<script_dir>/Math_Symbol.js'], { stdio: 'inherit', cwd: <script_dir> })

setup.mjs 是一个经过深度混淆的下载器(dropper)。它的唯一任务是从 github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/ 静默下载 Bun JavaScript 运行时,并利用它来执行真正的载荷 Math_Symbol.jsexecFileSync(<bun binary>, ['<script_dir>/Math_Symbol.js'], { stdio: 'inherit', cwd: <script_dir> })

The Math_Symbol.js is a heavily obfuscated 728 KB JavaScript file containing credential stealers that harvest secrets from the victim’s environment, encrypt the findings, and exfiltrate them to a public GitHub repository whose description reads “Shai-Hulud: Here We Go Again”. The payload also contains worm-like propagation functionality to infect packages of other maintainers that have installed one of the compromised packages.

Math_Symbol.js 是一个 728 KB 的深度混淆 JavaScript 文件,其中包含凭据窃取程序。它会从受害者的环境中收集密钥,加密这些数据,并将其外泄到一个描述为“Shai-Hulud: Here We Go Again”(沙丘之虫:又来了)的公共 GitHub 仓库中。该载荷还具备蠕虫式的传播功能,能够感染其他安装了受损软件包的维护者的项目。

What it steals

窃取内容

The Math_Symbol.js file implements a set of credential extractors, each targeting a different secret store on the victim machine: Math_Symbol.js 文件实现了一套凭据提取器,分别针对受害者机器上的不同密钥存储:

  • npm tokens: Reads ~/.npmrc and scans the filesystem for any other .npmrc files. Extracts authToken values and any //registry.*:_authToken=... entries. Validates each token live against registry.npmjs[.]org/-/whoami before exfiltrating.

  • npm 令牌:读取 ~/.npmrc 并扫描文件系统中的其他 .npmrc 文件。提取 authToken 值及任何 //registry.*:_authToken=... 条目。在泄露前,会通过 registry.npmjs[.]org/-/whoami 实时验证每个令牌。

  • GitHub tokens: Three token formats are targeted: classic PATs (ghp_...) and OAuth tokens (gho_...), GitHub App server-to-server tokens (ghs_...), and JWT OIDC tokens. Sources include ~/.config/gh/hosts.yml, environment variables, and a filesystem scan. On GitHub Actions runners, the payload also executes a shell command that reads the runner process memory directly to dump the entire secret store. It reads ACTIONS_ID_TOKEN_REQUEST_TOKEN and ACTIONS_ID_TOKEN_REQUEST_URL to steal OIDC tokens used for npm publishing.

  • GitHub 令牌:针对三种令牌格式:经典 PAT (ghp_...) 和 OAuth 令牌 (gho_...)、GitHub App 服务器间令牌 (ghs_...) 以及 JWT OIDC 令牌。来源包括 ~/.config/gh/hosts.yml、环境变量和文件系统扫描。在 GitHub Actions 运行器上,该载荷还会执行 shell 命令直接读取运行器进程内存,以转储整个密钥存储。它会读取 ACTIONS_ID_TOKEN_REQUEST_TOKENACTIONS_ID_TOKEN_REQUEST_URL 以窃取用于 npm 发布的 OIDC 令牌。

  • AWS credentials: ~/.aws/credentials and ~/.aws/config (parsing all named profiles), AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN environment variables, EC2 Instance Metadata Service (IMDSv2/v1), ECS container metadata endpoint, and AWS Secrets Manager (enumerating and exfiltrating all secrets).

  • AWS 凭据:读取 ~/.aws/credentials~/.aws/config(解析所有命名配置),获取 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_SESSION_TOKEN 环境变量,利用 EC2 实例元数据服务(IMDSv2/v1)、ECS 容器元数据端点以及 AWS Secrets Manager(枚举并泄露所有存储的密钥)。

  • Kubernetes secrets: Reads service account tokens, CA certificates, and namespaces from /var/run/secrets/kubernetes.io/serviceaccount/. Uses the token to query the Kubernetes API directly to retrieve all secrets. Also targets KUBECONFIG and ~/.kube/config.

  • Kubernetes 密钥:从 /var/run/secrets/kubernetes.io/serviceaccount/ 读取服务账号令牌、CA 证书和命名空间。使用服务账号令牌直接查询 Kubernetes API 以获取命名空间中的所有密钥。同时针对 KUBECONFIG~/.kube/config

  • HashiCorp Vault tokens: Checks six sources (environment variables, ~/.vault-token, GitHub Actions runner paths, container paths, Kubernetes auth login, and AWS IAM auth). After obtaining a token, it enumerates all KV stores via /v1/sys/mounts and reads every secret from KV v1 and v2 paths.

  • HashiCorp Vault 令牌:检查六个来源(环境变量、~/.vault-token、GitHub Actions 运行器路径、容器路径、Kubernetes 认证登录以及 AWS IAM 认证)。获取令牌后,通过 /v1/sys/mounts 枚举所有 KV 存储,并读取 KV v1 和 v2 路径下的所有密钥。

  • Stripe and Slack tokens: Scans for Stripe API keys (test and live) and Slack tokens across all files.

  • Stripe 和 Slack 令牌:扫描所有文件中的 Stripe API 密钥(测试和生产环境)及 Slack 令牌。

  • Generic filesystem scan: A platform-aware scanner runs ~200 glob patterns targeting .env files, private keys (*.pem, *.key, etc.), SSH keys, Terraform state, Docker registry credentials, KeePass databases, VPN configs, and IDE config files.

  • 通用文件系统扫描:一个具备平台感知能力的扫描器运行约 200 个 glob 模式,针对 .env 文件、私钥(*.pem*.key 等)、SSH 密钥、Terraform 状态文件、Docker 注册表凭据、KeePass 数据库、VPN 配置和 IDE 配置文件。

Exfiltration

数据外泄

Once credentials are harvested, the payload encrypts the entire bundle before sending it anywhere. 一旦凭据被收集,载荷会在发送前对整个数据包进行加密。