Back to blog
Security
IntermediateForPlatform EngineersSecurity EngineersDevSecOps
8 min

npm Supply Chain Attack Mitigation: What Actually Works

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.

npm-supply-chain-attacksupply-chain-securitytrusted-publishingnpm-provenancedevsecopsci-cd-security
Contents

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.

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.

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:

Terminal window
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:

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.

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: 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.

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

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.

Frequently asked questions

Does npm still run install scripts by default?

Not on npm v12. GitHub announced in June 2026 that npm v12 ships a breaking change disabling install scripts by default, because attackers use them to exfiltrate credentials at install time rather than waiting for the package to be imported at runtime. Scripts can be re-enabled by approving specific ones. The same release also blocks dependencies specified via git or remote URLs by default, which closed another install-time execution path. If you are still on an older npm, this is not done for you and you should set ignore-scripts yourself.

Does npm audit protect against supply chain attacks?

Not against a version published an hour ago. npm audit compares your tree against the advisory database, so something has to be found, reported and published first. GitHub did narrow this gap in August 2026 by wiring OpenSSF's malicious-packages data into the Advisory Database, so malware advisories now exist alongside CVEs. But an advisory still lags the attack, and the whole point of these campaigns is to spread inside that lag. Keep running audit; stop reading a clean result as proof you are not compromised.

What is the Dependabot cooldown?

Since July 2026, Dependabot version updates wait until a release has been available for at least three days before opening a pull request, so detection signals have time to surface before a malicious release reaches your project. It is on by default. Security updates are exempt and still open immediately, so critical fixes are not delayed. pnpm offers a stricter version of the same idea through its minimumReleaseAge setting, which refuses to install a version that is younger than the age you configure.

What is npm trusted publishing?

An OIDC trust relationship between npm and your CI provider, so a named workflow can publish without a long-lived token. Each publish uses a short-lived, cryptographically signed credential that cannot be extracted from logs or reused. It follows the OpenSSF trusted publishers standard already adopted by PyPI, RubyGems, crates.io and NuGet. It supports GitHub Actions, GitLab CI/CD and — since April 2026 — CircleCI, on cloud-hosted runners only, and requires npm CLI 11.5.1 or later with Node 22.14.0 or higher.

What happened to classic npm tokens?

They are gone. npm's documentation states that as of November 2025 only granular access tokens are supported and legacy access tokens have been removed. Granular tokens let you scope by package, scope and organisation, restrict by IP range, and set an expiry at least one day out. Watch the Bypass 2FA option: it is false by default, but when enabled it overrides both account-level and package-level 2FA for publishing.

What is staged publishing?

An opt-in npm control released in May 2026. A publish does not go live on credentials alone — the version is staged until a maintainer approves it with 2FA in the npm CLI or on npmjs.com. That decouples the credentials your CI pipeline holds from the ability to actually distribute a package, which cuts the chain from a compromised pipeline to malware reaching users.

From the community

Discussion on the Fediverse

Replies from Mastodon and Bluesky — straight from the open web, no tracking.

Loading replies …

ENDE