The package manager settings that would have blocked the Axios attack
07 Apr 2026On the 31st of March 2026, attackers hijacked an npm maintainer account and published malicious versions of axios with a remote access trojan baked in. npm pulled the bad releases after about two or three hours, but that was enough. Anyone who ran npm install axios during that window could have installed the trojan. The article Post Mortem: axios npm supply chain compromise has all the details.
This kind of attack keeps happening and the playbook barely changes: compromise an account, push a malicious update, hope people install it before anyone notices, get removed a few hours later.
Every major package manager now lets you defend against this. Here is the 30-second setup for each one.
Why the Axios attack worked
The malicious versions were published as ordinary patch releases. If your project depended on "axios": "^1.14.0" and performed a fresh resolution without a lockfile pinning a safe version, install could resolve to the poisoned version automatically. And if the attacker included a postinstall script, npm ran it during installation without asking.
So one npm install could download the compromised package, run arbitrary code on your machine or CI runner, and exfiltrate credentials, tokens, SSH keys, source code - whatever it wanted.
The whole thing hinges on two assumptions: you install the bad version quickly, and npm runs the install script. The three settings break both.
1. Delay new releases
min-release-age=7
This tells npm to skip any version published in the last 7 days.
Seven days sounds like a lot. But most malicious releases get caught within hours. The Axios compromise was live for roughly 2-3 hours. With this setting, npm would have refused to resolve those versions until they were a week old, by which point they'd already been yanked.
Every major package manager has an equivalent, though the config key and units differ. npm calls this setting min-release-age (in days); pnpm and Bun call it minimumReleaseAge (minutes and seconds respectively); Yarn uses npmMinimalAgeGate (human-readable durations).
# pnpm-workspace.yaml (minutes, so 10080 = 7 days)
minimumReleaseAge: 10080
# bunfig.toml (seconds, so 604800 = 7 days)
[install]
minimumReleaseAge = 604800
# .yarnrc.yml
npmMinimalAgeGate: '7d'
2. Disable install scripts globally
ignore-scripts=true
npm packages can define lifecycle hooks (preinstall, install, postinstall) and by default npm runs them automatically. Attackers exploit this because their code executes during installation. You don't even need to import the package.
With ignore-scripts=true, npm downloads the package but refuses to run any install-time code. The install-time RAT payload described in the Axios compromise would not have executed during install.
The tradeoff
Some legitimate packages need install scripts, especially native modules: sharp, bcrypt, sqlite3, esbuild, puppeteer. If something breaks, rebuild only the packages you trust:
npm ci --ignore-scripts
npm rebuild sharp esbuild
You can also override per project if you need to:
# project/.npmrc
ignore-scripts=false
3. Pin exact versions
save-exact=true
By default npm saves dependencies with a caret (^1.14.0), which means future patch and minor versions get pulled in automatically. That's the convenience that made the Axios attack possible.
With save-exact=true, npm records "axios": "1.14.0". No range, no floating. You still update dependencies, but through a PR or lockfile update. Not because someone pushed a release five minutes ago.
30-second setup for your package manager
Pick yours, copy the config, and you are done.
npm
Requires npm v11.10.0 or later. To make sure your team is on a compatible version, add engines to your package.json and set engine-strict=true in .npmrc.
# ~/.npmrc (or project .npmrc)
ignore-scripts=true
min-release-age=7
save-exact=true
engine-strict=true
pnpm
minimumReleaseAge uses minutes. 10080 = 7 days.
# pnpm-workspace.yaml
minimumReleaseAge: 10080
onlyBuiltDependencies:
- sharp
- esbuild
pnpm can block arbitrary install scripts using onlyBuiltDependencies. Once configured, only the listed packages may run build/install scripts. Without it, lifecycle scripts still run normally.
Bun
minimumReleaseAge uses seconds. 604800 = 7 days. Bun is more restrictive by default: it does not run arbitrary dependency lifecycle scripts, uses a built-in allowlist for many common packages, and lets you allow additional ones with trustedDependencies in package.json. If you want to disable lifecycle scripts entirely, use bun install --ignore-scripts.
# bunfig.toml
[install]
minimumReleaseAge = 604800
// package.json
{
"trustedDependencies": ["sharp", "esbuild"]
}
Yarn (current releases)
npmMinimalAgeGate accepts human-readable durations. Disable scripts globally and allow them per package with dependenciesMeta.
# .yarnrc.yml
npmMinimalAgeGate: '7d'
enableScripts: false
dependenciesMeta:
sharp:
built: true
esbuild:
built: true
Lockfiles and CI
Even with these settings, commit your lockfile. A committed package-lock.json (or pnpm-lock.yaml, bun.lock, yarn.lock) means CI gets the same dependency graph every time, fresh installs don't resolve to something unexpected, and dependency updates show up in PRs where you can actually review them.
In CI, always use the lockfile-only install command for your package manager:
npm ci # not npm install
pnpm install --frozen-lockfile
bun install --frozen-lockfile
yarn install --immutable
npm install in CI can silently update the lockfile and pull in new versions. npm ci refuses to do that. The other managers behave the same way with their respective flags.
Commit the lockfile. Review lockfile diffs. Update dependencies on purpose. Don't let CI grab whatever happens to be newest.
Why this combination works
The Axios attack needed three things to go right: the victim installs quickly, the dependency range floats to the bad version, and install scripts run automatically.
| What the attacker needs | What blocks it |
|---|---|
| Install before the package is noticed | min-release-age=7 |
| Auto-upgrade to the newest release | save-exact=true |
| Execute malware during install | Disable install scripts / allowlist trusted packages |
Most supply chain attacks follow the same pattern. These settings won't stop everything, but they would have stopped this one, and they will probably stop the next few too.
Stay safe out there
Scroll up to the 30-second setup, pick your package manager, and paste the config.
Commit your lockfiles, use lockfile-only installs in CI, review dependency changes in PRs, and keep an allowlist for packages that genuinely need install scripts.