It seems like nowadays popular npm packages get compromised regularly: for example Axios or chalk have been successfully targeted, following a similar pattern: a maintainer’s account gets hijacked, allowing an attacker to publish a malicious version of that package. As a result, anyone running npm install in the following few hours gets their machine or environment compromised. This appears to be the new reality we live in.
I’ve been lucky enough not to be compromised, but relying on luck alone isn’t acceptable anymore. Turns out we’ve already got plenty of tools at our fingertips to help mitigate this attack vector, but they need to be set up first. So here are a few simple steps that you should enable now to harden yourself against future supply chain attacks.
ā¹ļø Note: Nothing here is bulletproof and it all still relies on a reactive & proactive JS community but each step closes off a real attack vector that’s been used before: one less chance you’ll be the next victim.
Tl;dr #
To harden your project against npm supply-chain attacks:
- Add to
.npmrc:min-release-age=3dandignore-scripts=true npm install --save-dev @lavamoat/allow-scripts lockfile-lint --ignore-scripts=falsenpx allow-scripts setupnpx can-i-ignore-scriptsto find packages that need scripts- Fill in
lavamoat.allowScriptsinpackage.jsonbased on the output - Call
lockfile-lintfrom your lint step
The Threats #
Before piling on tools, it helps to be clear about what you’re defending against.
- Compromised maintainer account
Examples: Axios (March 2026), chalk & debug (September 2025), Shai-Hulud worm (September 2025)
In practice: happens repeatedly, several known incidents per year
What helps:min-release-age+ signatures - Malicious install scripts
Examples: Nx s1ngularity (August 2025), Axios (March 2026)
In practice: common, usually paired with a maintainer account compromise
What helps:ignore-scripts - Lockfile injection
Examples: safedep PoC (2023), Snyk write-up (2019)
In practice: demonstrated as an attack vector but not seen at scale in the wild
What helps:lockfile-lint - Registry tampering or MITM
In practice: hypothetical but could be achieved in theory
What helps:npm audit signatures - Weaponised CVE advisory
In practice: theoretical but plausible
What helps:min-release-agequarantine
(I’m surprised the CVE advisory angle hasn’t been attempted yet (as far as we know) as it could increase the impact of a supply-chain attack significantly. If you’re a malicious actor and reading this, please forget I ever mentioned this)
The Changes #
1. A 3-day quarantine via min-release-age #
This is by far the biggest win for the smallest effort. Two lines in .npmrc:
# .npmrc
min-release-age=3d
npm will then refuse to resolve any version published less than 3 days ago. Most supply chain attacks get caught and yanked within hours, so by simply not being first, you sidestep almost all of them.
ā ļø Requires npm 11.10+, which ships with Node 22+.
2. Block install scripts by default #
This is the Nx-style attack. A malicious package’s postinstall script runs the moment you install it, and now it has whatever permissions your shell has.
The fix could be as simple as turning install scripts off entirely:
# .npmrc
ignore-scripts=true
Of course, some packages legitimately need install scripts to function properly. npm doesn’t have built-in ways to allowlist specific packages, so I turned to @lavamoat/allow-scripts which gives you a per-package allowlist config in package.json:
{
"lavamoat": {
"allowScripts": {
"prisma": true,
"@prisma/client": true,
"sharp": true,
"puppeteer": true,
"esbuild": false,
"msw": false
}
}
}
Then to set it all up:
npm install --save-dev @lavamoat/allow-scripts --ignore-scripts=false
npx allow-scripts setup
š” The setup command adds
ignore-scripts=trueto your.npmrcand installs@lavamoat/preinstall-always-failas a tripwire: if someone (you, future-you, a contributor) removesignore-scripts, the next install fails loudly.
To figure out which packages actually need scripts in your project, the community-built can-i-ignore-scripts comes in handy:
npx can-i-ignore-scripts
I went through can-i-ignore-scripts output and decided which packages I wanted to allow and which could be left blocked (for example, esbuild is a pure ESM package that doesn’t actually need install scripts to work, so it can be left blocked).
3. Verify package signatures #
npm audit signatures
This checks every installed package against the npm registry’s Sigstore signature. It catches registry tampering, MITM attacks, etc.
4. Lint the lockfile #
lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https --validate-integrity
This makes sure your package-lock.json only points at the official npm registry, uses HTTPS, and has integrity hashes for every package. Protects against injections of a resolved URL pointing at an attacker-controlled host.
New Routine #
From the changes listed above, only min-release-age takes effect without any
change to your workflow. The other ones require you to run the checks manually
after every install (which, let’s be honest, you’ll forget to do most of the time).
My workaround is a tiny shell function (here with Zsh):
npm() {
if [[ "$1" == "install" || "$1" == "ci" || "$1" == "i" ]]; then
command npm "$@"
echo "\nā” Running post-install security checks (custom npm wrapper)..."
echo "š Verifying package signatures..."
command npm audit signatures
if [[ -f "node_modules/@lavamoat/allow-scripts/src/cli.js" ]]; then
echo "\nš Running allowed install scripts..."
command npx allow-scripts
fi
else
command npm "$@"
fi
}
After every npm install / npm ci / npm i, this verifies signatures and runs the allowlisted install scripts. The allow-scripts step only fires if it’s installed in the current project, so this is safe to drop in ~/.zshrc even for projects that haven’t been hardened yet.
The lint command goes alongside other lint checks.
CI/CD #
Your local shell wrapper won’t follow you into CI, so the same checks need to be wired into your pipeline explicitly.
After npm ci, run npm audit signatures, npx allow-scripts, and lockfile-lint as separate steps so a failure points at the exact culprit.
The .npmrc settings (min-release-age, ignore-scripts) are applied automaticlly.
This Isn’t Fullproof #
Let’s be realistic: you’re still running code from untrusted sources right on your machine. If a legitimate maintainer publishes malicious code (intentionally or not) that sits for more than 7 days before anyone notices, and the attack doesn’t rely on post-install scripts to do damage, you’re toast. And we all are.
