How npm worms steal your credentials, and the order to rotate them in
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 s
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, [email protected] 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/[email protected] 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=truein~/.npmrcand 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 loginuses the OS keyring when one is available. For publishing from CI, use npm trusted publishing instead of a long-livedNPM_TOKEN. - Don't pass secrets to the
npm installstep 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.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.