npm audit: Broken by Design
Found 99 vulnerabilities (84 moderately irrelevant, 15 highly irrelevant)
npm audit: Broken by Design
July 7, 2021
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
- [email protected]
- [email protected]
- [email protected]
- [email protected]
- [email protected]
Most likely, it’s a lot deeper.
Now say there’s a vulnerability discovered in [email protected]:
your-app
- [email protected]
- [email protected]
- [email protected]
- [email protected]
- [email protected] (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 [email protected] with the fix in it. As long as database-layer specifies that it depends not on exactly on [email protected] but some permissible range that includes 1.0.1, the fix “just works” and you get a working application:
your-app
- [email protected]
- [email protected]
- [email protected]
- [email protected]
- [email protected] (Fixed!)
Alternatively, maybe [email protected] depends strictly on [email protected]. In that case, the maintainer of database-layer needs to release a new version too, which would allow [email protected] instead:
your-app
- [email protected]
- [email protected]
- [email protected]
- [email protected] (Updated to allow the fix.)
- [email protected] (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.
Let’s create a new project!
npx create-react-app myapp
Immediately upon creating a project, I see this:
found 5 vulnerabilities (3 moderate, 2 high)
run `npm audit fix` to fix them, or `npm audit` for details
Oh no, that seems bad! My just-created app is already vulnerable!
Or so npm tells me.
Let’s run npm audit to see what’s up.
First “vulnerability”
Here is the first problem reported by npm audit:
┌───────────────┬──────────────────────────────────────────────────────────────┐
│ Moderate │ Regular Expression Denial of Service │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package │ browserslist │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in │ >=4.16.5 │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ react-scripts │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path │ react-scripts > react-dev-utils > browserslist │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info │ https://npmjs.com/advisories/1747 │
└───────────────┴──────────────────────────────────────────────────────────────┘
Apparently, browserslist is vulnerable. What’s that and how is it used? Create React App generates CSS files optimized for the browsers you target. For example, you can say you only target modern browsers in your package.json:
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
Then it won’t include outdated flexbox hacks in the output. Since multiple tools rely on the same configuration format for the browsers you target, Create React App uses the shared browserslist package to parse the configuration file.
[...]