---
title: Poisoned packages — the npm worms
summary: 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.
topic: attacks
order: 2
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.0` means "take any newer 1.x", so a fresh malicious release is
  picked up automatically.
- `npm install` runs 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.1` and `0.30.4` of 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
  `keyv` and `cache-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

```ini
# .npmrc
min-release-age=7
ignore-scripts=true
```

`min-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

```yaml
- uses: some-org/some-action@8f4b7c2e1d...   # a full commit SHA, not @v4
```

A 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-scripts` breaks 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](https://www.cisa.gov/news-events/alerts/2026/04/20/supply-chain-compromise-impacts-axios-node-package-manager)
- [CISA — Widespread Supply Chain Compromise Impacting npm Ecosystem (Shai-Hulud)](https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem)
- [Microsoft — ChainDrop supply chain compromise](https://www.microsoft.com/en-us/security/blog/2026/08/04/chaindrop-supply-chain-compromise-anatomy-self-propagating-worm/)
- [OpenAI — Our response to the TanStack npm supply chain attack](https://openai.com/index/our-response-to-the-tanstack-npm-supply-chain-attack/)
- [npm Docs — Trusted publishing](https://docs.npmjs.com/trusted-publishers)
- Andres Freund, *backdoor in upstream xz/liblzma*, oss-security mailing list, 29 March 2024.
