NPM Supply Chain Hardening
There have been several high-profile supply chain attacks in the NPM ecosystem recently, including the tanstack compromise a few days ago, the recent Axios 'RAT' of Spring 2026, the first wave of "Shai-Hulud" worm in late 2025 and before that chalk and debug in September 2025 - and many more in between all these.
If you run a web application or Node.js service, this should be a concern. Every time you run npm install
you may be pulling a compromised version of a package - if that happens your local data is at risk, so are the stored
keys you use to access services, depending on the malicious code you may even spread the infection.
Just to provide some context, there have only been a few high-profile attacks in the NPM ecosystem recently and these have been short-lived. With the affected versions being rapidly removed from the registry within hours of being published, typically.
While it's unlikely these compromised you, the effect can be quite bad if by chance you were impacted.
How does this happen?
If you use Semver Range Operators referencing a version of a
dependency, such as axios@^0.26.0, when you run npm install you may pick up a new version of a package,
e.g. 0.26.25. That version may have been published just seconds prior, which is not enough time for the community to
find compromises and remove that package version.
If you pull in a library, it too may have Semver Range Operators, even if your package.json doesn't... so you are still at risk of inadvertently pulling a very recent version of a library. And so on for every library in your dependency tree.
How to defend against this
Configure a minimum dependency age and have a lockfile control process.
It's much more likely that a one-week-old compromised package version will be found and removed than a 10-minute old compromised package. That time window probably won't affect your development, but it will impact your risk profile.
Treat lockfile changes like any other change, review it and control changes.
What won't work
Simply using NPM + lockfile is not enough for an actively developed product. If you at any point replace the lockfile
(e.g. by doing an npm install) then NPM could be pulling in a very recently published version of a dependency at
that moment (particularly for older package manager versions). If you release a new version once a year it's a small
risk, and clearly the risk is higher the more often you regenerate the lockfile.
Equally, pinning dependencies with exact versions in your package.json is not enough either - while your direct dependencies won't change, those transitive dependencies are outside your control. Your dependencies will most likely contain at least some Semver Range Operators, and again may pull in a very recently published dependency.
What will work
Minimum Release Age
If you have one takeaway from this article, it's this. Ensure you are using a package manager that has a "minimum release age" setting.
Minimum Release Age settings prevent you from pulling in libraries (even if they're transitive dependencies) that are below a certain age. That means any dependencies that you use will have been likely seen by more people and are more likely to be safe.
Recent versions of dependency managers have a minimum release age.
- npm (v11.10+, February 2026) (Node 24+): Uses min-release-age in days (default is min-release-age=1, override in .npmrc).
- pnpm (v10+, November 2025): Uses minimumreleaseage in minutes (default is minimumreleaseage=1440 (1 day), override in pnpm-workspace.yaml).
- yarn (v4.10+, October 2025): Uses npmMinimalAgeGate in days (default is off, set in .yarnrc)
In addition to this, consider setting the Minimum Release Age to a setting equivalent to 1 week. Why 1 week rather than the default 1 day? Some compromises will take longer to find than others. The real danger zone is in the "minutes" time range. The original Shai-Hulud compromise was found within about 1 day. The recent Tanstack compromise was first reported within 20 minutes. A 1 week minimum gives you a safety buffer as the community has more time to find novel attacks and de-list them.
Use pnpm rather than npm
PNPM is compatible with npm's package.json, but has a number of security features that make it a better choice.
PNPM disables build script triggers by default, unless you explicitly enable them (it warns you in the build process). This completely prevents newly added triggers from running without you seeing it, helping you to spot sudden additions of build scripts. This is one of the most common serious attack vectors since the script has shell access, can read and exfiltrate files, compromise keys, etc.
The lockfile must be protected!
Only allow the lockfile (and the config file) to be updated by the team lead or CI/CD pipeline (i.e. an authoritative git user). Reject merges that change the lockfile by anyone other than the authoritative git users (in GitHub you can use a Ruleset for this, Jenkins can do it with a script, etc).
Why? It further locks in known-good versions of the libraries and reduces the risk of pulling in a compromised version of a library without someone being aware that the libraries have changed.
pnpm install ignores the age limit if the version is already in your lockfile. see this issue
npm install does this too. see here
Without a check like this, a developer could by mistake pull in a compromised version of a library while trying to get a
cutting edge feature (for example NPM allows you to override this setting with a parameter to the install command),
which wouldn't be caught by the CI/CD pipeline.
Developer considerations
Make the right path the easy path. Create package.json scripts that use the correct commands that use frozen versions, encourage developers to use it. e.g.
NPM
{
...
"scripts": {
"install-deps": "npm ci"
},
...
}
PNPM
{
...
"scripts": {
"install-deps": "pnpm install --frozen-lockfile"
},
...
}
You can of course override any dependency and add it to the lockfile if needed (remember, lock file versions are not checked against minimumreleaseage), you have to balance the risk of recent dependencies with the cost of (not) having a bleeding-edge version.
Building deployables
Similar to the previous section, when building the deployable (e.g. use the command pnpm install --frozen-lockfile)
to ensure that the lockfile is strictly used.
If you use the bare install version, the package manager will try to upgrade the version of libraries, which can cause
the version that is deployed to be different to the one you developed against.
This applies to both the CI/CD pipeline and (in the case of Node.js services) in the production environment if you deploy node libraries from source (e.g. with PM2, services, or similar).
Build Automation
You could automate the upgrade PR process by setting up a CI process to install updates to the lockfile with the same 1 week buffer as above (minimumreleaseage). Create a PR automatically and get a human to review it.
... or simply do this manually with a privileged git user.
pnpm audit will tell you if there are any known vulnerabilities in your dependencies - building it into your
CI/CD upgrade pipeline may give better risk visibility.
🔥 Avoid: Pinning your versions in package.json
I've seen advice along the lines of;
- "pin your dependencies"
- "package.json should depend on exact versions only"
- "don't use '^' or '~' wildcard dependencies unless you know what you're doing"
This used to be our strategy, we assumed it caused the dependency tree to be immutable. It doesn't.
Pinning to exact dependency versions feels safer on the face of it, but this won't actually prevent you from pulling in a compromised version of a library in general due to transitive dependencies (see What won't work). And the automation in previous sections wouldn't work as a result (to pull in genuine bug fixes past the maturity threshold).
As such, I advise against this approach if you have the above defensive measures. Using a lock file will provide a consistent dependency, pinned versions in package.json just give the impression of that.
In my opinion, you should use caret (^) or tilde (~) versions to keep getting those bugfixes and security updates - and use the above steps to make it safer to do.
Final thoughts
I suspect features and defaults settings will eventually cover all of these steps. In the short term, this is how I'll be managing my supply chain.
These settings and features only reduce risk from recent vulnerabilities that are found and removed from NPM - it does nothing to prevent older compromised dependencies that have gone undetected (assuming that these exist in the wild).
