three hours of axios@1.14.1
On March 31, 2026, someone got into the axios npm maintainer account and published 1.14.1, and it sat there live for roughly three hours before it got yanked. That is not a long time in human terms. It is an eternity in CI terms, because every pipeline on the planet that runs npm install on a fresh checkout, without a lockfile, resolving a caret range like ^1.14.0, happily pulled the poisoned version the moment it hit the registry. axios is a dependency of a dependency of a dependency for basically every Node project that has ever existed, so the blast radius was enormous even in three hours.
The payload itself was almost boring in how well it followed the playbook. The published tarball declared a new transitive dependency, a package nobody had heard of, that existed purely to carry a postinstall script. That script pulled down a platform-specific binary, a cross-platform remote access trojan that worked on Linux, macOS, and Windows runners, and it fired the second the install completed. No exploit, no clever memory corruption, nothing you'd write a CVE about. Just npm doing exactly what npm was designed to do, which is run arbitrary code from strangers on your machine during install.
Attribution landed on Sapphire Sleet, the same crew that has been running social-engineering-into-supply-chain campaigns for years now. The interesting part was never the malware. It was how they got publish rights, and what the industry's supposedly-solved answer to that problem completely failed to do.
why OIDC trusted publishing did nothing
Here is the part that should ruin your weekend if you run a JavaScript-heavy shop. The axios repo had OIDC Trusted Publishing configured. GitHub Actions minting a short-lived, workflow-scoped OIDC token, npm verifying that token against a trusted publisher config, no long-lived credentials sitting around to steal. That is the thing every security post for the last two years has told you to adopt, and axios adopted it. It provided exactly zero protection here.
It provided zero protection because Trusted Publishing was configured *alongside* a classic long-lived npm automation token that had never been revoked. The account still had a granular access token in the npm settings, the kind you generate once and paste into a secret somewhere and forget about for eighteen months. Sapphire Sleet didn't touch GitHub Actions at all. They didn't need to compromise a workflow or forge an OIDC claim or exploit the trust config. They got the long-lived token through account takeover and ran npm publish from a laptop like it was 2019.
This is the foot-gun almost nobody talks about. Setting up Trusted Publishing does not disable your existing tokens. npm treats them as two independent, valid paths to publish, and having the modern one does not close the legacy one. So teams walked away from their migration feeling like they'd hardened everything, and what they actually did was add a second front door while leaving the first one wide open, with a token that never expires and probably has publish scope on every package in the org.
Go check right now. npm token list for every account that can publish anything you depend on internally. If you migrated to Trusted Publishing and there is still an automation token in that output, you have this exact problem, and no OIDC config in the world will save you from it.
who got hit and who didn't
The teams that came out clean fall into two groups, and the difference between them tells you what actually mattered. The first group had committed lockfiles and CI that ran npm ci instead of npm install. npm ci refuses to touch package-lock.json, it installs the exact resolved tree that was already there, and since nobody's lockfile pointed at 1.14.1 until they deliberately updated it, those pipelines kept pulling 1.14.0 straight through the three-hour window like nothing happened. Pinning your deps, or more precisely committing a lockfile and installing from it, worked. It's just that everybody says that and half of us don't do it in every pipeline, especially not in the throwaway ones.
The second group had lockfiles that *did* resolve the bad version, because their Dependabot or Renovate bot happened to open a PR during the window, but they still didn't get owned. Those teams ran their installs with --ignore-scripts. The malicious version got resolved and downloaded, the tarball landed on disk, and the postinstall hook simply never fired. The RAT was sitting right there in node_modules doing absolutely nothing, because the one thing it needed, npm executing its install script, was globally disabled.
Everybody else got a coin flip. If your CI ran a bare npm install on a fresh clone with no lockfile during those three hours, or you were rebuilding Docker layers without lockfile caching, you very likely executed the payload on a runner that had access to your registry credentials, your cloud secrets, and whatever else lives in a build environment. Which is everything.
the concrete changes
Drop --ignore-scripts into your CI installs today. npm ci --ignore-scripts for npm, pnpm install --ignore-scripts for pnpm, and if you're on Yarn Berry set enableScripts: false in .yarnrc.yml. This is the single highest-leverage change and it costs you almost nothing, because the packages that genuinely need install scripts are a short, known list (native modules like better-sqlite3, sharp, some Prisma internals) and you allow-list exactly those with pnpm's onlyBuiltDependencies or by running a scoped rebuild step for the handful that need it. Ninety-plus percent of your dependency tree has no business running code at install time, so stop letting it.
Audit for the dual-auth pattern next. For every npm org you publish under, list active tokens and revoke any long-lived automation token that Trusted Publishing was supposed to replace. If you migrated to OIDC, the migration isn't done until the old token is gone, full stop. Same logic applies to PyPI trusted publishing and to any registry where you bolted a modern flow on top of a legacy one without deleting the legacy path.
Make npm ci mandatory. If a pipeline anywhere in your org runs npm install on a build agent, that's a bug. Commit lockfiles, fail the build when the lockfile is out of sync, and never let CI resolve fresh ranges against a live registry, because that is precisely the window this attack drove a truck through.
And separate your update flow from your build flow. Dependency bumps should land in a branch, get built with scripts disabled, get scanned, and only merge after a human or a policy looks at what actually changed in the resolved tree. The three-hour version being auto-merged by an over-eager bot is how a couple of teams turned a near-miss into an incident.
what we changed on our own pipelines
We build a lot of Next.js and Node backends at steezr, and this one made us go audit our own house before we said a word to clients. Every CI install across our repos now runs with scripts disabled by default, with a tiny per-project allow-list for the two or three packages that legitimately need native builds, and that allow-list lives in the repo where a reviewer sees it in diffs. We found one internal tooling package with a stale npm automation token from a 2024 setup that predated our Trusted Publishing move, exactly the pattern that bit axios, and we killed it that afternoon.
The part that took longer was the Docker story. A lot of our build images were doing COPY package.json then npm install, which throws away the lockfile guarantee entirely inside the container even when the host repo has a perfect lockfile. We switched every Dockerfile to copy both package.json and package-lock.json and run npm ci --ignore-scripts, which also made our layer caching dramatically better as a bonus.
None of this is exotic. It's the boring hygiene that everyone nods along to in a postmortem and then quietly deprioritizes because the pipeline works and shipping features feels more urgent. The axios event is a clean, recent, real example you can put in front of leadership when you need budget for a week of unglamorous CI cleanup. Three hours, one long-lived token nobody remembered, a postinstall hook, and a RAT on runners that could read every secret you own. That's the whole story, and the fixes are a day of work you're not doing yet.
