npm audit: Broken by Design

Published on
·
4 min read
0
Security is important. Nobody wants to be the person advocating for less security. So nobody wants to say it. But somebody has to say it.
So I guess I’ll say it.
The way npm audit works is broken. Its rollout as a default after every npm install was rushed, inconsiderate, and inadequate for the front-end tooling.
Have you heard the story about the boy who cried wolf? Spoiler alert: the wolf eats the sheep. If we don’t want our sheep to be eaten, we need better tools.
As of today, npm audit is a stain on the entire npm ecosystem. The best time to fix it was before rolling it out as a default. The next best time to fix it is now.
In this post, I will briefly outline how it works, why it’s broken, and what changes I’m hoping to see.
Note: this article is written with a critical and somewhat snarky tone. I understand it’s super hard to maintain massive projects like Node.js/npm, and that mistakes may take a while to become apparent. I am frustrated only at the situation, not at the people involved. I kept the snarky tone because the level of my frustration has increased over the years, and I don’t want to pretend that the situation isn’t as dire as it really is. Most of all I am frustrated to see all the people for whom this is the first programming experience, as well as all the people who are blocked from deploying their changes due to irrelevant warnings. I am excited that this issue is being considered and I will do my best to provide input on the proposed solutions! 💜
How does npm audit work? Skip ahead if you already know how it works.
Your Node.js application has a dependency tree. It might look like this:
your-app
  • view-library@1.0.0
  • design-system@1.0.0
  • model-layer@1.0.0
  • database-layer@1.0.0
  • network-utility@1.0.0 Most likely, it’s a lot deeper.
Now say there’s a vulnerability discovered in network-utility@1.0.0:
your-app
  • view-library@1.0.0
  • design-system@1.0.0
  • model-layer@1.0.0
  • database-layer@1.0.0
  • network-utility@1.0.0 (Vulnerable!) This gets published in a special registry that npm will access next time you run npm audit. Since npm v6+, you’ll learn about this after every npm install:
1 vulnerabilities (0 moderate, 1 high) To address issues that do not require attention, run: npm audit fix To address all issues (including breaking changes), run: npm audit fix --force You run npm audit fix, and npm tries to install the latest network-utility@1.0.1 with the fix in it. As long as database-layer specifies that it depends not on exactly on network-utility@1.0.0 but some permissible range that includes 1.0.1, the fix “just works” and you get a working application:
your-app
  • view-library@1.0.0
  • design-system@1.0.0
  • model-layer@1.0.0
  • database-layer@1.0.0
  • network-utility@1.0.1 (Fixed!) Alternatively, maybe database-layer@1.0.0 depends strictly on network-utility@1.0.0. In that case, the maintainer of database-layer needs to release a new version too, which would allow network-utility@1.0.1 instead:
your-app
  • view-library@1.0.0
  • design-system@1.0.0
  • model-layer@1.0.0
  • database-layer@1.0.1 (Updated to allow the fix.)
  • network-utility@1.0.1 (Fixed!) Finally, if there is no way to gracefully upgrade the tree, you could try npm audit fix --force. This is supposed to be used if database-layer doesn’t accept the new version of network-utility and also doesn’t release an update to accept it. So you’re kind of taking matters in your own hands, potentially risking breaking changes. Seems like a reasonable option to have.
This is how npm audit is supposed to work in theory.
As someone wise said, in theory there is no difference between theory and practice. But in practice there is. And that’s where all the fun starts.
Why is npm audit broken? Let’s see how this works in practice. I’ll use Create React App for my testing.
If you’re not familiar with it, it’s an integration facade that combines multiple other tools, including Babel, webpack, TypeScript, ESLint, PostCSS, Terser, and others. Create React App takes your JavaScript source code and converts it into a static HTML+JS+CSS folder. Notably, it does not produce a Node.js app.