跳到正文
DEV Community· quietkernel·· 3 小時前精選AI 評分76

npm 蠕蟲如何竊取憑證,以及憑證輪替次序

How npm worms steal your credentials, and the order to rotate them in

AI 導讀

npm 蠕蟲可利用套件生命週期指令碼竊取開發者憑證,文章提出遭入侵後按風險安排的輪替次序。npm 的 preinstall、install 和 postinstall 指令碼會以用戶權限執行,能讀取用戶可讀檔案並發出網絡請求。作者建議先隔離及清除惡意程式,再撤銷 npm 等套件 registry token,之後處理 GitHub、雲端、CI 和其他憑證,並在乾淨裝置上進行輪替。

推薦理由

文章按蠕蟲的散播風險排列憑證輪替步驟,解釋為何應先清除持續執行的惡意程式,再撤銷套件 registry token。

正文 · 原文

Almost every npm supply-chain advisory ends with the same instruction: rotate every credential the machine could access. It sounds simple until you try it at 2 AM. Which credentials did the machine have? Where are they stored? And does the order matter?

The order does matter. In at least one recent case, doing the obvious thing first deletes your home directory.

This post walks through what a malicious package can read on a typical developer machine, why worms in particular go after some credentials before others, and a rotation order that holds up. The worked example at the end uses a small open-source CLI, but the order applies whether you use a tool or a checklist on paper.

How a malicious package gets to run

npm packages can declare lifecycle scripts in package.json: preinstall, install and postinstall. When you run npm install, npm runs those scripts with your user's permissions. No prompt, no sandbox.

{
  "name": "innocent-looking-lib",
  "version": "1.2.4",
  "scripts": {
    "preinstall": "node ./setup.js"
  }
}

Whatever is in setup.js can read any file your user can read and make network requests. That is the whole attack. Two real examples from this month show what the payloads do in practice.

On October 8, 2026, tensorlake@0.5.144 was published from a compromised maintainer account. According to the project's own advisory (GHSA-8g63-53c9-65j2), the payload ran from a preinstall hook and harvested "GitHub and npm tokens, cloud provider credentials (AWS, GCP), Kubernetes and Vault tokens, SSH and GPG keys, .env files, browser-saved passwords, cryptocurrency wallets, and AI tool and MCP configuration." It then used the stolen tokens to republish itself into other packages the victim could write to. The version was live for about an hour and forty minutes, and it shipped with valid npm provenance because the attacker published through the project's own release workflow.

A few days earlier, @subql/common@5.8.3 shipped a postinstall hook that decoded and ran an obfuscated payload. The OSV entry MAL-2026-17571 has the analysis.

Neither of these is new as a technique. CISA's September 2025 alert on the Shai-Hulud worm describes the same pattern: steal tokens, then use them to publish infected versions of the victim's own packages.

What a stealer reads first

Most of these payloads don't need anything clever. Developer credentials sit in well-known plaintext files:

File What's in it
~/.npmrc //registry.npmjs.org/:_authToken=..., your publish token
~/.config/gh/hosts.yml, ~/.git-credentials GitHub tokens, when no OS keyring is in use
~/.aws/credentials AWS access keys, often long-lived
~/.kube/config cluster tokens and client keys
~/.ssh/id_* private keys, sometimes without a passphrase
.env files in your projects database URLs, payment keys, webhook secrets
MCP configs (~/.cursor/mcp.json, Claude Desktop config, etc.) API keys pasted directly into env blocks
process.env in CI whatever secrets the job passes to the install step

You can check your own exposure for npm in one line:

grep -E '_authToken|_auth|_password' ~/.npmrc

If that prints a literal token (not ${NPM_TOKEN}), any install script on this machine can read it.

Why the order matters

When a machine is compromised, you're racing two things: the malware that may still be running, and the stolen credentials that are already being used elsewhere. Rotating in the wrong order can let the attacker re-steal what you just rotated, or set off something worse.

Here is the reasoning, step by step.

1. Stop the malware first. If the payload installed something persistent, every credential you rotate from that machine gets stolen again. The tensorlake payload made this concrete. Its advisory says it installs a service named gh-token-monitor "that deletes the user's home directory if the stolen GitHub token is revoked," and tells you to remove it before rotating any GitHub token. So the first step is to disconnect the machine, remove persistence, and do every rotation after this from a different, clean machine.

On Linux the advisory's removal steps are:

systemctl --user disable --now gh-token-monitor.service
rm -rf ~/.config/gh-token-monitor ~/.local/bin/gh-token-monitor.sh

On macOS:

launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
rm ~/Library/LaunchAgents/com.user.gh-token-monitor.plist

Also check crontab -l, systemd user units, LaunchAgents and .vscode/tasks.json for anything you didn't add.

2. Package registry tokens next. This is what makes a worm a worm. A stolen npm token lets the attacker publish an infected version of every package you maintain, which then runs on every machine that installs it. Every minute that token is live, the infection can spread past you. Revoke it:

npm token list
npm token delete <token-id>

npm's own docs note that revocation can take up to an hour to take effect. Then run npm view <your-package> time for each package you maintain and look for versions you didn't publish. The same logic covers PyPI, RubyGems and crates.io tokens.

3. GitHub. GitHub tokens let the attacker push commits, add workflows and read private repos. They come after npm because they're slightly less direct as a spreading channel, and they come after step 1 because of the hostage-service trick above. Revoke tokens at github.com/settings/tokens and OAuth apps at /settings/applications, then gh auth logout and gh auth login.

4. Cloud and infrastructure. AWS, GCP, Azure, Terraform, Kubernetes. These are often the most damaging credentials, but they don't spread the worm to other people, so they come after the spreading channels. For AWS, create a new key before you delete the old one so you don't break running systems, and check CloudTrail for reads of SSM Parameter Store and Secrets Manager during the exposure window.

5. Deploy platforms and CI. Anything stored in a platform's env store, plus the secrets your CI passes to steps. Remember to redeploy, because most services read environment variables at startup and keep the old values until they restart.

6. AI provider keys, SSH keys, and application secrets. Still important, still need doing, but they don't let the attack spread.

So the order is: stop the code that's running, close the channels the attack uses to spread, then clean up everything else, most damaging first.

Worked example: encoding the order in a tool

I wrote this order into a small CLI called secret-blast-radius, so a scan can turn "rotate everything" into a concrete list. The interesting part is how little code the ordering needs. Each finding the scanners produce has a category, and the categories map to a table in src/index.js:

export const ROTATION = {
  persistence: { order: 0, label: 'Stop the malware first (worm services, malicious packages/extensions)', /* steps, verify, docs */ },
  npm:         { order: 1, label: 'Package registry tokens: npm, PyPI, RubyGems, crates.io, Composer (stops a worm republishing your packages)' },
  github:      { order: 2, label: 'GitHub tokens and sessions' },
  cloud:       { order: 3, label: 'Cloud and IaC credentials (AWS, GCP, Azure, Terraform, Pulumi)' },
  kubernetes:  { order: 4, label: 'Kubernetes and container registries' },
  paas:        { order: 5, label: 'Deploy platforms (Railway, Vercel, Fly, Render, Heroku, Netlify)', redeploy: true },
  env:         { order: 6, label: 'Secrets present in the environment / CI', redeploy: true },
  ai:          { order: 7, label: 'AI provider keys and MCP servers' },
  ssh:         { order: 8, label: 'SSH keys' },
  dotenv:      { order: 9, label: 'Application secrets in .env files', redeploy: true },
  // ...
};

(Trimmed. Each entry also carries steps, a verify check that the old credential is dead, and links to the provider's docs.)

The runbook is then a filter and a sort. It keeps only the categories where the scan actually found something, so you don't get a 12-step plan for a machine that only has an npm token and an SSH key:

export function runbook(result) {
  const cats = [...new Set(result.findings.map((f) => f.category))]
    .filter((c) => ROTATION[c])
    .sort((a, b) => ROTATION[a].order - ROTATION[b].order);
  return cats.map((c) => ({ category: c, ...ROTATION[c], items: result.findings.filter((f) => f.category === c) }));
}

Findings in the normal report are sorted by severity first and rotation order second, so two critical findings show up with the persistence one above the npm token.

If you write your own tooling, one choice I'd copy is that findings meaning "you're already infected" go into the persistence category, not their own. A known-bad version in a lockfile, a worm marker dependency, or a VS Code task that runs a disguised file all land in step 0. If you've installed a malicious version, the first job is cleaning the machine, not rotating keys from it.

The scanner checks the plaintext locations from the table above and prints a short fingerprint (the first 8 hex characters of a SHA-256) instead of the value, so you can tell two tokens apart without the tool ever printing a secret.

To try it:

npx secret-blast-radius            # scan and print findings plus the rotation order
npx secret-blast-radius --runbook  # Markdown checklist with steps and verify checks
npx secret-blast-radius --harden   # read-only audit of settings that would have limited the damage

Making the next one hurt less

Rotation is cleanup. A few settings reduce what the next payload can reach:

  • Set ignore-scripts=true in ~/.npmrc and allow-list the few packages that need build scripts. pnpm already blocks dependency build scripts by default.
  • Add a release-age cooldown so freshly published versions aren't installed straight away. pnpm has minimumReleaseAge. The tensorlake version was live for under two hours.
  • Keep tokens out of plaintext files. gh auth login uses the OS keyring when one is available. For publishing from CI, use npm trusted publishing instead of a long-lived NPM_TOKEN.
  • Don't pass secrets to the npm install step in CI. Give them only to the steps that need them.

One caveat on provenance: it tells you where a package was built, not whether the code is safe. The tensorlake advisory points out that the malicious version carried valid provenance, because the attacker used the project's own workflow.

Stop the malware, revoke the tokens that let it spread, then rotate the rest from a clean machine.


Disclosure: I maintain secret-blast-radius.

來源:DEV Community · dev.to