---
title: "npm Supply Chain Attack Mitigation: What Actually Works"
description: "The npm ecosystem changed a lot during 2026. What is now secure by default, what is opt-in and worth turning on this week, and which gaps are still entirely yours to close."
author: Aleksei Aleinikov
date: 2026-09-09
lang: en
tags: [npm-supply-chain-attack, supply-chain-security, trusted-publishing, npm-provenance, devsecops, ci-cd-security]
canonical: https://www.alekseialeinikov.com/en/blog/topics/security/npm-supply-chain-attack-mitigation-2026
source: alekseialeinikov.com
---

# npm Supply Chain Attack Mitigation: What Actually Works

If you last read about npm supply chain security after the Shai-Hulud worm, your mental model is a year out of date. A lot shipped during 2026, and some of the advice everyone was giving is now done for you by default.

This is what is actually true today: what you get for free, what is still opt-in, and what nobody is going to fix on your behalf.

![npm supply chain attack mitigation: what is default, what is opt-in, and what is still on you.](https://www.alekseialeinikov.com/blog/npm-supply-chain-attack-mitigation-2026.webp)

## The attack this all responds to

GitHub was notified on **September 14, 2025** of the **Shai-Hulud** attack. Worth restating because every control below is shaped by it:

- It got in through **compromised maintainer accounts**, not a vulnerability in any package.
- It injected malicious **post-install scripts** into popular packages.
- It was **self-replicating** — a worm.
- It stole **multiple kinds of secrets**, not just npm tokens.

GitHub removed **500+ compromised packages** and blocked uploads matching the malware's indicators of compromise.

Every package involved was one you meant to install, from a maintainer you had reason to trust, at an unremarkable version number.

## Why your scanner said the tree was clean

`npm audit` compares your dependency tree against an **advisory database**. For a finding to exist, somebody must first discover, report and publish it.

A malicious version pushed twenty minutes ago has been through none of that. No advisory, no match, clean report.

**One honest update:** in August 2026 GitHub wired OpenSSF's malicious-packages data into the Advisory Database, so malware advisories now sit alongside CVEs. The gap is narrower than it was.

It is not closed. An advisory still trails the attack, and spreading inside that lag is the entire business model. Keep running `npm audit`. Just stop treating a clean result as evidence you are fine.

![Why npm audit reports a clean tree: the advisory gap that this attack class lives inside.](https://www.alekseialeinikov.com/blog/npm-audit-blind-spot-2026.webp)

## What you now get by default

This is the part that changed, and the reason most 2025-era advice needs rereading.

### npm v12 disables install scripts

Announced June 2026 as a breaking change: **npm v12 disables install scripts by default**. Attackers used them to exfiltrate credentials at install time rather than waiting for your code to import anything. You re-enable them by approving specific scripts.

The same change **blocks dependencies specified via git or remote URLs** by default, closing another install-time execution path.

This is the single most important line in this article. The vector Shai-Hulud actually used is off by default — **if you are on v12**.

If you are not, it is still your job:

```bash
npm ci --ignore-scripts
```

Or in `.npmrc`:

```
ignore-scripts=true
```

**The honest cost:** packages with native components need their build step. A blanket ban across a large monorepo will break installs. Deny by default and allowlist the exceptions — the model pnpm has used for a while via `allowBuilds`, with `dangerouslyAllowAllBuilds` if you want out entirely. The name tells you what the maintainers think of that.

### Dependabot waits three days

Since July 2026, Dependabot version updates **wait until a release is at least three days old** before opening a pull request. Detection signals get time to surface first. **On by default**, and **security updates are exempt** — critical fixes are not delayed.

Attackers depend on speed. This removes it, at almost no cost to you.

pnpm enforces a stricter version at install time:

```yaml
# pnpm-workspace.yaml
minimumReleaseAge: 10080   # minutes — one week
```

A version younger than that is not eligible for installation at all. `minimumReleaseAgeExclude` handles the packages you want immediately.

### Classic tokens are gone

npm's docs are unambiguous: **as of November 2025, only granular access tokens are supported. Legacy access tokens have been removed.**

Granular tokens scope by package, scope and organisation, restrict by IP range, and take an expiry at least one day out.

**One footgun worth knowing:** granular tokens have a **Bypass 2FA** option. It defaults to false, but when enabled it **takes precedence over both account-level and package-level 2FA** for publishing. If you enforce 2FA on a package and someone has a bypass token, your enforcement is not what you think it is.

Since **August 2026**, bypass-2FA tokens can no longer perform account-identity or governance actions — changing email or password, touching 2FA config, managing tokens, adding maintainers. Those always require an interactive challenge. They can still publish directly.

### Compromised accounts get a cooling-off period

Since June 2026, high-impact npm accounts drop into **read-only mode for 72 hours** when the email changes or a 2FA recovery code is used. Phishing is how these campaigns start; this buys the real maintainer time to recover before the account can publish.

## What is still opt-in

Defaults handle the last attack. These are what you turn on yourself.

### Trusted publishing

OIDC between npm and your CI provider. A named workflow is authorised to publish. Each publish mints a short-lived, signed credential that **cannot be extracted from logs or reused**.

```yaml
permissions:
  id-token: write     # required for OIDC
  contents: read

steps:
  - uses: actions/checkout@v6
  - uses: actions/setup-node@v6
    with:
      node-version: '24'
      registry-url: 'https://registry.npmjs.org'
      package-manager-cache: false   # never cache in release builds
  - run: npm ci
  - run: npm test
  - run: npm publish
```

Constraints to plan around:

- npm CLI **11.5.1+**, Node **22.14.0+**
- GitHub Actions, GitLab CI/CD, and **CircleCI since April 2026**
- **Cloud-hosted runners only** — self-hosted is not supported
- Up to **10 trusted publishers** per package

Same idea as [killing service account keys with Workload Identity Federation](https://www.alekseialeinikov.com/en/blog/topics/security/kill-service-account-keys-workload-identity-federation-2026): stop storing a long-lived credential, mint a short-lived one per run against a verified identity.

![The npm publish path with a long-lived token versus trusted publishing with OIDC.](https://www.alekseialeinikov.com/blog/npm-trusted-publishing-2026.webp)

Publishing this way from GitHub Actions or GitLab CI/CD generates **provenance attestations automatically** — no `--provenance` flag. Not supported for CircleCI, and not generated for packages published from **private repositories**, even when the package is public.

### Staged publishing

Released May 2026, opt-in. Credentials alone no longer distribute a package: the version is **staged until a maintainer approves it with 2FA**, in the CLI or on npmjs.com.

This is the control that survives a fully compromised pipeline. An attacker with your CI credentials gets a staged version and a notification to a human, not a release.

### Close the doors you stopped using

Setting up trusted publishing while the old automation token stays valid gains you little. Once the new flow works:

1. Package Settings → Publishing access → **"Require two-factor authentication and disallow tokens"**
2. Revoke the automation tokens you no longer need

And when configuring 2FA, npm's guidance is explicit: **WebAuthn, not TOTP**. TOTP codes are phishable.

### Watch what leaves your runner

GitHub's **Actions network firewall** is in technical preview and logs all outbound traffic from workflow runs, so you can spot a job suddenly talking to a domain it has never talked to before.

This is the only item here that is **detection** rather than prevention, which makes it disproportionately valuable.

## What none of this fixes

The section the vendor posts skip.

**A compromised maintainer can still publish malicious code.** Trusted publishing proves the package came from the right workflow in the right repository. It says nothing about whether that repository's code is benign. Given commit access, provenance will faithfully attest to a malicious build.

**Install scripts are not the only execution path.** npm v12 closing them raises the cost of the current pattern. It does nothing if the payload is in the library you import and run.

**Cooldowns are a trade, not a win.** You accept a window running a version with a published vulnerability, to avoid being first to run a malicious one. Usually correct. Still a trade — which is exactly why Dependabot exempts security updates.

**Almost none of this is detection.** Every control above lowers the odds of executing malicious code. Only egress monitoring tells you that you already did. Assume the answer might be yes, and know how fast you can rotate every secret a build job can see.

## Where to start

| Control | Effort | Status |
| --- | --- | --- |
| Move to npm v12 (install scripts off by default) | Hours | **Default once upgraded** |
| Dependabot cooldown | None | **Already on** |
| Read-only tokens for installs; no publish token in build jobs | Hours | On you |
| Trusted publishing + disallow tokens | Hours | Opt-in |
| Staged publishing | Hours | Opt-in |
| `minimumReleaseAge` (pnpm) | Hours | Opt-in |
| Audit granular tokens for the Bypass 2FA flag | Minutes | On you |
| WebAuthn instead of TOTP | Minutes | On you |
| Egress monitoring on CI runners | Days | Preview |

Two things this week: **upgrade to npm v12, or set `ignore-scripts` until you can**, and **get publish tokens out of every job that also installs dependencies**. That covers the path Shai-Hulud actually used.

## The bottom line

The 2025 advice was "do these things yourself because nobody else will". Through 2026 the ecosystem made the two highest-impact ones — no install scripts, no instant updates — the default.

That is real progress, and it also means the remaining risk has moved. It now sits in the things still switched off: trusted publishing, staged publishing, and any idea of what your build machines talk to.

`npm audit` will keep telling you the tree is clean. It is answering a narrower question than the one you should be asking.
