← Back to list

The Axios attack lasted three hours. Here’s what I shipped before the AI-generated next one lands.

On March 30–31, 2026, two malicious versions of axios: npm’s most-downloaded HTTP client, around 100 million weekly downloads - went live…

Gal Efraty · 2026-05-24 18:29 · 5 claps · 11.2 min read
#cybersecurity #ai #software-supply-chain #frontend #ai-agent
Open on Medium ↗
Wiki topics: AGT · AI Agents AI · AI · General MAC · Macroeconomics 🌐 · Web Development 🔒 · Cybersecurity

The Axios attack lasted three hours. Here’s what I shipped before the AI-generated next one lands.

On March 30–31, 2026, two malicious versions of axios: npm’s most-downloaded HTTP client, around 100 million weekly downloads - went live on the registry for about three hours. Versions 1.14.1 and 0.30.4 both bundled a transitive dependency called plain-crypto-js, a typosquat of crypto-js that had been seeded the day before, and which on install pulled down a remote access trojan for macOS, Windows, and Linux. Google's Threat Intelligence Group later attributed the campaign to UNC1069, a North Korea-nexus financially motivated actor. The maintainer's npm account had been hijacked through a months-long social engineering chain involving a fake Slack workspace branded to look real.

If you want the full incident reconstruction, Unit42 and Google Cloud Threat Intel wrote it up better than I can. This article isn’t about what happened. It’s about what I did the day after, and why.

I work at a supply chain security company. We sell vendor risk management — the discipline of treating your supply chain as a risk surface, of asking “if this vendor is compromised, what’s our blast radius?” That’s our product. That’s how we describe ourselves to customers. So when the alert about axios landed, the question wasn’t academic. We use axios. Our frontend uses axios. If we shipped a compromised version downstream to customers who buy our product partly because we tell them to treat their supply chain seriously, the credibility cost is asymmetric — the kind where any defense you didn’t have in place becomes the only thing anyone remembers.

So I spent that day on a diff.

What we started with, what we changed, and what we built

I want to be honest up front: we weren’t at zero. The article you usually read about a security incident is structured as “we had nothing, then disaster struck, then we fixed everything.” That’s almost never the real shape. Real engineering teams sit somewhere on the spectrum, with some defenses in place and some gaps they hadn’t gotten around to closing.

What was already there:

  • Snyk was integrated in our CI pipeline. Every PR ran a vulnerability gate. New criticals failed the build.
  • **package-lock.json was committed.** This sounds obvious. It isn't universal. The lockfile is what makes "install the exact thing my last build installed" possible. Without it, every developer and every CI run resolves dependencies fresh from the registry, and "fresh from the registry" is exactly where compromised versions live for the brief window they're up.
  • Lockfile diffs were code-reviewed. A growing diff in package-lock.json from a small package.json change is one of the cleanest signals of a problem. We didn't treat lockfile changes as auto-merge noise.
  • Security-sensitive packages were pinned to exact versions. No carets. Anything that touches network, auth, file parsing, or sanitization.
  • CI used npm ci, not npm install. A small difference that matters: npm ci refuses to run if the lockfile and package.json are out of sync, which catches some classes of tampering.
  • some more security layers

So when axios landed, we weren’t going to be one of the affected teams. we were protected. this time.

Two things to flag before the diff. First: what follows is one slice of the supply chain posture I’m building around this repo. There are other defenses, and other in-flight work, that I’m not naming here on purpose. The point of this article isn’t to publish a complete control inventory. It’s to share a way of thinking about a class of problem. Second: the techniques I’m about to describe are not novel. They’re documented in Mondoo’s 2026 supply chain write-up, in Chainguard’s analysis of the npm update, in pnpm’s official supply chain guide. What I think is useful here is the integration — what it looks like to actually run them together in one repo — and the why behind each one.

Here are the four configuration changes we shipped in the days after Axios.

1. Block install scripts — the silent code-execution vector that delivered the Axios payload

# .npmrc
ignore-scripts=true
// package.json
{
  "trustedDependencies": ["<package-name>", "<package-name>", "<package-name>"]
}

When npm installs a package, it can run lifecycle scripts defined in the package’s package.json: preinstall, install, postinstall. These hooks run with your user's full privileges. They can read ~/.ssh, ~/.aws/credentials, your ~/.npmrc token. They are how the axios payload executed - not when anyone used axios, but when anyone installed it.

ignore-scripts=true blocks all lifecycle scripts by default. trustedDependencies is the allowlist of packages whose scripts you let run anyway, because they legitimately need to — typically build tooling that downloads platform-specific binaries at install time, or anything that sets up local repo state like git hooks. Keep the list short, and review it whenever it grows.

The allowlist beats a blocklist on this class of problem. A blocklist requires you to know about a bad package before it can harm you, which is the exact assumption the axios attack defeated.

2. Add a 72-hour quarantine on new package versions

# .npmrc
min-release-age=3

This is the change I’m most opinionated about. min-release-age=3 tells npm to refuse to install or resolve any package version that was published less than three days ago. If a package you've requested has only a fresh version available, npm falls back to the previous version that meets the age requirement.

The axios versions were live for about three to four hours before they were yanked. A 72-hour quarantine wouldn’t have made the window smaller — it would have made the window irrelevant for us. We wouldn’t have been able to install those versions even if we’d tried.

Honest caveat: this doesn’t help against slow-burn campaigns. An attacker who seeds a malicious version, sits on it for two weeks, and then promotes it via some other vector defeats a three-day quarantine. min-release-age is calibrated against the specific failure mode where the malicious version is live and getting installed while the community is still detecting it. That's most of what we've seen in 2025–2026. It isn't all of it.

One ergonomic point: min-release-age was added to npm in version 11.10, which shipped in November 2025. For most of last year the consensus was "if you want install-time cooldown, switch to pnpm or Yarn Berry, because npm doesn't support it." That's no longer true. Which brings me to the unglamorous prerequisite: we upgraded from Node 22 (npm 10) to Node 24 (npm 11) to get this feature.

3. Verify every package resolves from the official npm registry over HTTPS

We added lockfile-lint to our CI pipeline. It walks package-lock.json and fails the build if any package resolves from anywhere other than [https://registry.npmjs.org.](https://registry.npmjs.org.)

This catches lockfile tampering — a class of attack where the manifest stays clean but the lockfile is silently rewritten to point at a malicious source. It’s the npm equivalent of a DNS poisoning that no human would notice in a 2,000-line lockfile diff. It’s also incredibly cheap to add: two minutes of CI configuration.

4. Check package signatures and provenance — catch forged passports

We added npm audit signatures to the pipeline. This validates npm's signature and provenance attestations: cryptographic proof that a package was published from the source repository and CI workflow it claims to have been published from.

The right mental model: signatures don’t penalize a package for not having a passport. They catch packages with a forged one. A package missing provenance is just a package missing provenance. A package whose provenance is tampered with fails CI.

The anecdote no one tells you about

These changes are not friction-free. There was a real moment in the middle of the Sunday work where npm audit signatures and min-release-age were briefly at odds — one was rejecting installations the other had just authorized, and the error messages were unhelpful. It took an hour and a separate commit to normalize the two against each other.

I mention this because most blog posts about security hardening read like brochures. The real version of this work has hours that look like that one. If you’re staring at a CI failure that doesn’t make sense after enabling these features, you’re not doing anything wrong. You’re doing it correctly.

The fifth thing: a bounded AI agent

Configuration changes are a one-shot. Vulnerabilities are continuous. Even with everything above in place, npm audit keeps surfacing new CVEs every week, transitive dependencies bump, your repo accumulates noise. The honest version of running a frontend repo today is that periodic human triage of npm audit output isn't scaling. Either you let it rot, or you find a way to automate the bounded parts.

So I built a local Claude Code subagent that does this:

It runs npm audit --json and classifies every finding into three groups. Group A is auto-fixable (a patched version exists, no major bump required). Group B needs a manual bump or an override. Group C can't be fixed right now — no patched version is available, or the only fix would require a major-version bump on a package I've already classified as "migration, not patch." For Group A, the agent runs npm audit fix without --force, then runs a verification triad: npm test, npm run build, npm run lint. If anything fails, it reverts. For Group B, it researches the package's release notes, applies the bump one package at a time, runs the triad again, and reverts if anything breaks. For Group C, it writes a report.

The credibility move — and the reason I’m comfortable writing about an AI agent at all — is that the agent has a list of things it can’t do, written into its instructions. It can’t run npm audit fix --force, because that makes multiple breaking changes simultaneously. It can't perform major-version upgrades on the handful of packages I've flagged as "migration, not fix" — those require engineering judgment, not automation. for example, am major react version, It can't unpin a security-sensitive package, because the pinning is intentional. It can't add anything to trustedDependencies. It can't modify .npmrc. It can't install anything published in the last 72 hours. There are about a dozen of these explicit prohibitions, sitting at the top of the agent's instructions, where they're the first thing the model reads on every run.

That’s the difference between “I built an AI to do security” — which would be cringey if I said it — and “I built a bounded automation with an explicit safety surface.” The agent runs on a leash. The leash is what makes it usable.

The reason this matters in the larger story is that npm audit output on a real frontend repo is now too large and too noisy for periodic human review to be the only thing standing between you and a known vuln in production. You need automation. The question is what shape the automation takes. A bounded agent with a written-down list of off-limits operations is a shape I think holds up.

The judgment layer the config doesn’t cover

Every change I described above operates after a package has been added to the repo. A .npmrc rule, a CI check, an audit agent — none of them help if the question "should we add this package at all?" was never asked.

This is the part of the story where I sound like someone’s senior engineer, and I’m going to do it anyway because it matters. When you’re considering a new npm dependency:

  • Look at the weekly downloads on npmjs.com. Anything below 100,000 is a different risk class than anything above. That’s not a rule; it’s a heuristic.
  • Look at the maintainer count and the last-published date. Single-maintainer packages with long gaps between releases are the typical compromise target.
  • Ask whether the package is a wrapper around code you could write in four lines. The classic is-promise lesson — the package that brought down half the npm ecosystem in 2020 — never stops being relevant.
  • Prefer well-known dependencies over micro-packages, even if the micro-package is cleaner. Attack surface beats elegance for security.
  • Google/Claude/Gemini/ChatGPT/.. <package-name> vulnerability before adding. Five-second sanity check.
  • Ask whether the failure mode of not having the dependency is actually worse than the failure mode of having it.
  • Or automate this layer too. The same questions above can be wrapped in a bounded AI agent — pass it a package name and have it surface downloads, maintainer health, last-published date, known CVEs, and typosquat suspicion as an advisory recommendation before the install lands in your repo. Treat its output as a faster first pass, not a substitute for the human judgment it’s modeling.

One adjacent recommendation worth raising while we’re here: encrypt your sensitive environment variables. Frontend repos accumulate .env files, deploy-time secrets, and API keys that often get treated with less rigor than the code itself. A compromised npm package with ignore-scripts=false can read those files in under a second. Whether you do it through a managed secrets store, sealed secrets in your CI, or platform-native KMS — pick one and use it. The right amount of attention to give your env files is "the same amount you give your code." Most teams aren't at that level.

The bigger point of this section: configs and agents are floors. Judgment is the ceiling. No amount of min-release-age saves a repo that takes 200 dependencies without thinking. The question "do we actually need this?" is the same question at every layer of an organization — from a package install to a feature spec to a strategic bet. The engineering version of that question is what makes the supply chain tractable. The non-engineering version is what makes everything else tractable. They're the same skill.

Why this is just the floor

Everything I just described would have blocked axios. None of it will block what’s coming.

I want to make an argument about that, and I want to be careful. Predictions about security tend to age badly. But there’s a structural shift underway that I don’t think is reversible, and it’s worth naming.

npm has roughly 3 million packages and roughly 10,000 new versions published every day. Community detection of malicious versions scales with attention — with the number of humans paying attention, the speed at which they can investigate, and the channels they use to coordinate. Publish volume doesn’t scale with attention. Publish volume scales with whatever’s pushing it. And what’s pushing it now is increasingly not humans.

On the maintainer side, AI-assisted authoring means more legitimate packages, more frequent versions of existing packages, and more transitive dependencies per repo — a bigger attack surface to defend, growing faster than human triage can keep up with.

On the attacker side, AI eliminates most of the labor of credible-looking attacks. plain-crypto-js — the malicious transitive that axios pulled in — was seeded the day before as a clean typosquat of crypto-js. Generating that kind of plausible package name, structure, and initial README is a task AI now does for almost free, at scale. Dependency confusion attacks, credible-looking maintainer profiles, plausible commit history — the entire setup phase of a supply chain attack has had its cost cut to near zero.

Socket’s automated scanner caught the malicious axios transitive in six minutes. Six minutes is the new floor for detection. The defenses I described in this article are the floor for prevention. The bounded agent is the floor for remediation. None of these are sufficient. They’re just no longer optional. Human vigilance at scale was the load-bearing assumption of the npm ecosystem for fifteen years. That assumption is breaking, in real time, because the velocity gap between human review and machine publish keeps widening.

That’s why we use AI on the defense side too. Not as a substitute for engineers — as a force multiplier with safety rails. The agent I described above isn’t the answer. It’s a worked example of the genre of defense the next five years will require.

There are still defenses none of this addresses. Long-dwell campaigns, where an attacker seeds clean versions for months before turning them malicious. Compromise of trusted maintainers, where your allowlist hands the attacker the keys. The Shai-Hulud worm class — self-propagating malware that crosses package boundaries via maintainer relationships. These are not solved by anything in this article. I want to be clear about that.

Closing

Three months ago I would have written this article from the analyst’s seat — what other companies should do, what mistakes other teams make. Today I write it from the maintainer’s seat — what we actually changed, what we still can’t defend against, and what I think is coming next.

That’s the gap I think about now. It isn’t closing. The next axios is already in someone’s draft folder, waiting on the right opportunity. The job isn’t to be invulnerable. The job is to be unattractively expensive to target, and to make the next defense after this one before anyone needs you to.

The four lines in our .npmrc are not the answer. They're the floor. If your frontend repo isn't on at least this floor, this week is a good week to fix it.


메타데이터
post_id
08ba500d092b
slug
the-axios-attack-lasted-three-hours-heres-what-i-shipped-before-the-ai-generated-next-one-lands-08ba500d092b
url
https://medium.com/@galefraty/the-axios-attack-lasted-three-hours-heres-what-i-shipped-before-the-ai-generated-next-one-lands-08ba500d092b
canonical_url
https://medium.com/@galefraty/the-axios-attack-lasted-three-hours-heres-what-i-shipped-before-the-ai-generated-next-one-lands-08ba500d092b
author_url
https://medium.com/@galefraty
status
ok
fetched_at
2026-06-09 15:37:30