Learn / Attack archive / Lesson 21
Poisoned packages — the npm worms
How one phished maintainer turned into hundreds of infected packages, why npm install runs code before you look at it, and the settings that buy you a week of safety.
Last updated: 2026-09-23
What it is
A supply chain attack does not attack your code. It attacks code you pull in from someone else: a package, a build tool, a CI action. When the attacker controls a package you depend on, their code runs on your laptop, your build server and your production server, with your permissions.
The npm ecosystem is the favourite target because of three facts:
- A normal project pulls in hundreds of packages, most of them dependencies of dependencies you never chose.
- A version range like
^1.14.0means "take any newer 1.x", so a fresh malicious release is picked up automatically. npm installruns each package's lifecycle scripts (preinstall,postinstall) by default. The malware runs before any test, review or scanner sees it.
Why it is a rule
- xz-utils, March 2024. Over two years a contributor earned the trust of the maintainer of a compression library used by almost every Linux system, then slipped a backdoor into the release files that let them log in to any affected server over SSH. It was caught by chance, weeks before it reached stable Linux releases, because a Microsoft engineer noticed SSH logins were half a second slow.
- tj-actions/changed-files, March 2025. A GitHub Action used by more than 23,000 repositories was changed to print every CI secret into the build logs. Public repositories leaked their secrets to anyone who opened the log.
- chalk and debug, 8 September 2025. A maintainer got a convincing "update your 2FA" email from a fake npm domain and typed in their login. Within hours, 18 packages with about 2 billion weekly downloads shipped code that swapped crypto wallet addresses in browsers.
- Shai-Hulud, September and November 2025. The first self-spreading npm worm. When it ran on a developer's machine it stole their npm token, then published infected versions of every package that developer maintained. Each new victim spread it further. Hundreds of packages were hit in days, and a second wave in November hit hundreds more.
- Axios, 31 March 2026. Versions
1.14.1and0.30.4of the most popular HTTP client added a new dependency,plain-crypto-js, which downloaded a remote-access trojan. CISA issued an alert. - TanStack, 11 May 2026. A "Mini Shai-Hulud" wave hit the TanStack libraries. OpenAI reported that two employee laptops ran it, that credentials in some internal repositories were stolen, and that it rotated its app-signing certificates as a precaution, forcing every macOS user to update.
- ChainDrop, August 2026. More than 400 packages from unrelated publishers, including
keyvandcache-manager. Microsoft found it stole npm, GitHub, AWS, Kubernetes and Vault credentials, republished itself with each stolen npm token, and wrote files into repositories' AI-assistant and editor settings so it would run again later.
Why it keeps working
- One password guards millions of installs. Most worms start with a single phished maintainer.
- Install equals execute. Lifecycle scripts give the attacker code execution the moment the package lands.
- Developer machines and CI hold the keys. npm tokens, cloud keys and SSH keys sit in plain files and environment variables, so the worm finds its next ride right away.
- New versions are trusted instantly. Most malicious releases are found within a day or two, but automatic updates pull them in within minutes.
How to do it
1. Install from a lockfile, always
Commit package-lock.json and install with npm ci in CI and on servers. That installs the exact versions you tested, not "whatever is newest".
2. Wait before trusting a new version
# .npmrc
min-release-age=7
ignore-scripts=truemin-release-age refuses packages published in the last seven days. Nearly every poisoned release above was pulled within that window. ignore-scripts stops lifecycle scripts from running; the few packages that truly need a build step can be run by hand or allowed one by one.
3. Keep secrets out of reach of installs
- Give CI jobs short-lived credentials (OIDC) instead of long-lived tokens.
- Do not keep a publish-capable npm token on your laptop. Use npm's trusted publishing so only your CI can publish.
- Run installs in a job that has no deploy secrets.
4. Protect the maintainer, if you publish
Use a passkey or security key for npm and GitHub, not a code from an app or SMS. The chalk phish would have failed against a key, because a key refuses to sign in to the wrong domain.
5. Pin CI actions by commit
- uses: some-org/some-action@8f4b7c2e1d... # a full commit SHA, not @v4A tag like @v4 can be moved to malicious code. A commit hash cannot.
6. Keep the list short
Every dependency is a stranger with commit rights to your server. Before adding one, ask whether twenty lines of your own code would do.
How we do it here
Dependencies are pinned by a committed lockfile, and the list is kept deliberately short: the server uses a small set of well-known packages, and features that could be written in a few lines are written in-house. Build and minify steps run before deploy, so the running server does not need build tooling.
Benefits
- A lockfile plus a release-age delay stops most of these attacks without any scanner.
- Short-lived CI credentials turn a stolen token into something that expires in minutes.
- Fewer dependencies means fewer maintainers whose inbox you have to trust.
Disadvantages
- A release-age delay also delays security fixes. You need a way to override it for an urgent patch.
ignore-scriptsbreaks packages with native builds until you allow them.- None of this helps if a package was malicious for months, as with xz-utils.
- Writing things in-house means you own their bugs.
Checklist
- A lockfile is committed and installs use
npm ci. - New package versions wait before they are installed.
- Lifecycle scripts are off, or allowed per package.
- CI uses short-lived credentials, and install steps have no deploy secrets.
- Publishing accounts use passkeys or security keys.
- CI actions are pinned by commit hash.
Sources
- CISA — Supply Chain Compromise Impacts Axios npm
- CISA — Widespread Supply Chain Compromise Impacting npm Ecosystem (Shai-Hulud)
- Microsoft — ChainDrop supply chain compromise
- OpenAI — Our response to the TanStack npm supply chain attack
- npm Docs — Trusted publishing
- Andres Freund, backdoor in upstream xz/liblzma, oss-security mailing list, 29 March 2024.