Arrange Act Assert

Jag Reehals thinking on things, mostly product development

The package manager settings that would have blocked the Axios attack

07 Apr 2026

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

security npm supply-chain