2025-11-06 · 3 min read

The Audit Fix That Made It Worse

npm audit offered to trade one advisory I could not reach for fourteen I could, and reading them took less time than the upgrade would have.

security · npm · dependencies

npm audit flagged react-router during a routine dependency pass on this site, and my hand was already on npm audit fix before I had read anything. That reflex is the part worth writing down. The command is right often enough that it has trained me not to look, and this was one of the times looking mattered.

What the fix was actually proposing

The version the tool wanted to move me to carries fourteen advisories of its own, one of them remote code execution. So the trade on offer was one advisory for fourteen, and the fourteen were worse.

That is not a bug in npm audit. It compares the version ranges in my lockfile against the version ranges in a database, and it will happily resolve one constraint by satisfying it with a release that violates several others, because it has no opinion about the destination beyond "the thing I was asked about is no longer flagged."

The question the tool cannot answer

The advisory against the pinned version is real. It is also, on inspection, scoped to React Server Components mode.

This app has no RSC. It has no server actions. The router runs entirely in the browser against a plain Express API, and the code path the advisory describes does not exist in anything I ship. The vulnerability is present in the dependency and unreachable in the application, and those are two different facts that a version-range comparison cannot distinguish.

Reachability is the question that decides what to do, and it is precisely the question the tooling is structurally unable to ask. It sees a dependency tree. It does not see a call graph, and it certainly does not see which modes of a library a given app has configured. Only the person who wrote the app knows that, which means the tool can raise the flag but cannot make the call.

What I did

I pinned it exact.

"react-router-dom": "7.18.2"

No caret. No range. The version is stated once and does not move on its own.

Then — the part that is more important than the pin — I wrote down why, in CLAUDE.md, where anyone touching this repo will read it before they touch dependencies:

react-router-dom is pinned exact. Do not let npm audit fix "fix" it — it proposes 7.11.0, which carries 14 advisories including RCE, versus one RSC-mode-only issue on the current pin that this app cannot reach.

A pin without a reason is indistinguishable from neglect. Six months from now the pin looks like something that got left behind, and the next person's reflex fires exactly the way mine did. The note is the difference between a decision and an accident.

What I gave up

npm audit on this repo is permanently noisy. It reports a finding on every run, forever, and I have removed my own ability to use a clean audit as a signal. That is a genuine loss and I do not have a clever answer for it.

The pin also has to be revisited by hand. Nothing will tell me when a version exists that resolves the advisory without importing the other fourteen. There is no alert for "the trade is good now"; there is only me remembering to look, which is a mechanism with a known failure rate.

And the reasoning could be wrong. I read the advisory, I checked how this app uses the router, and I concluded the path is unreachable. If I misread it, the consequence is that a real vulnerability sits in this repo behind a confident paragraph explaining why it does not count. Writing the reasoning down is partly so that someone can find the error, which only works if someone reads it again.

An advisory is an input. It tells you a thing exists in your tree. Whether it is reachable, whether the remedy is worse, and whether the trade is worth taking are all questions that stay with the person, and they do not get easier by being answered quickly.

All field notes