Hardening npm dependency security

Posted 22 April 2026 · 5 min read

Tags:

On March 30, 2026, two malicious versions of axios were briefly published to npm. Axios has over 100 million weekly downloads. The attacker had compromised a maintainer's account and used it to publish axios@1.14.1 and axios@0.30.4, each containing a hidden dependency whose postinstall hook silently installed a cross-platform Remote Access Trojan.

The malicious versions were live for around three hours before being removed, with Microsoft attributing the attack to a North Korean state actor. This was a targeted, well-prepared supply chain operation against one of the most widely used packages in the ecosystem.

It's a good reminder to review what you're actually doing to protect yourself.

Start with the obvious: use a lockfile

This one goes without saying, but it's worth saying anyway. Commit your lockfile. Don't run installs that bypass it. A lockfile means you're installing exactly what was resolved last time, not pulling whatever version satisfies your semver range today. It also means supply chain incidents show up as diffs so you can see when a transitive dependency changes unexpectedly.

Reduce your dependency surface area

The simplest way to limit exposure to a supply chain attack is to have fewer dependencies. Every package you don't install is a package that can't be compromised.

First, audit for unused dependencies. Knip will scan your codebase and surface packages that are listed in your package.json but no longer imported anywhere. Projects accumulate dead dependencies over time and most teams don't actively prune them. Running Knip periodically or including it in your CI pipeline makes sure unused dependencies are removed.

Second, look at e18e, an ecosystem initiative focused on cleaning up, modernising, and improving the performance of JavaScript packages. One aspect of that work is replacing heavy, outdated dependencies with lighter modern alternatives, such as is-odd style packages that have no reason to exist as a dependency, lodash functions that are now native, and so on. Less dependency weight means fewer packages to worry about.

pnpm settings

The axios RAT was delivered via a postinstall hook that runs automatically when a package is installed. This is the mechanism behind the majority of npm supply chain attacks.

pnpm v10 disables automatic execution of postinstall scripts in dependencies by default. Rather than running build scripts for any package that asks, you explicitly allowlist the ones that legitimately need it:

# pnpm-workspace.yaml
allowBuilds:
  esbuild: true
  "@parcel/watcher": true

If a dependency didn't require a build script before, it won't suddenly run one. A compromised version of a package can't use a postinstall hook to execute malicious code if that hook was never in the allowlist.

You should also enable a few pnpm settings to help protect you:

minimumReleaseAge: 10080 # 7 days in minutes
trustPolicy: no-downgrade
blockExoticSubdeps: true

minimumReleaseAge tells pnpm to refuse to install any package version published less than the specified number of minutes ago. The axios attack was live for three hours. A one-day delay (1440) would have been enough to dodge it entirely. We use seven days, matching our Renovate stability window.

With trustPolicy: no-downgrade if a package was previously published with provenance attestation from a trusted CI pipeline, but a new version lacks that evidence, pnpm will block the install. The axios attack was detectable this way as the malicious versions were published without the trusted publisher binding present in legitimate releases.

One caveat: no-downgrade does generate occasional false positives when maintainers of legitimate packages drop provenance attestation. You can use trustPolicyExclude to exempt specific packages you've manually verified:

trustPolicyExclude:
  - "some-package@1.2.3"

Also add blockExoticSubdeps: true explicitly. This prevents any transitive dependency from being resolved from a git repository or direct tarball URL, forcing them to come from the registry.

Scope your internal packages

If you publish internal packages to a private registry, make sure they're under an organisation scope (e.g. @myorg/package-name) rather than an unscoped name. This reduces the risk of a dependency confusion attack, where an attacker publishes a public package with the same or similar name as your internal one. If your registry configuration ever regresses or a new environment is misconfigured to pull from the public registry first, an unscoped internal package name is a straightforward target.

Automated upgrades with Renovate

We use Renovate to manage dependency updates across our projects. Two settings work together here.

minimumReleaseAge (formerly stabilityDays) delays Renovate from raising a PR for a new package version until it's been published for a given number of days. We set this to seven days. This gives the community time to catch malicious releases before they land in our codebase, and as a side benefit it avoids the churn of picking up a release that gets a patch two days later.

Security update PRs bypass the minimum release age entirely. If Renovate detects a known vulnerability in a dependency, it raises the PR immediately regardless of how recently the fix was published. The stability delay doesn't slow down your response to CVEs, it just slows down routine bumps.

For internal packages, configure a separate package rule with no stability delay. You want to roll those out quickly to catch integration issues early.

{
  "minimumReleaseAge": "7 days",
  "packageRules": [
    {
      "matchPackagePrefixes": ["@myorg/"],
      "minimumReleaseAge": "0 days"
    }
  ]
}

None of this is a silver bullet

These settings work by buying time rather than detecting anything themselves. We rely on the security researchers, automated scanning platforms and the open source community to identify compromised packages, along with the npm registry team to promptly remove them. A stability window only works if others find the issues, and it's not a replacement for vigilance around the dependencies you add and upgrades you pull in.


Get new posts by email

Subscribe to get new posts to your inbox, or use the RSS feed with your own feed reader.


Related posts · browse by tag

Caching & CDNs with micro-frontends

Published · 8 min read

How we've approached caching in a micro-frontend architecture

You can't trust agent tests

Published · 5 min read

Why passing tests aren't correct tests, and how to get an agent to actually validate them

Moving fast with agents without losing comprehension

Published · 6 min read

On test coverage, code authorship, and what reviewers actually need from you when agents are doing the writing