Transitive dependency vulnerability resolution for npm

This blog post uses npm, not yarn. While I imagine most of the concepts have equivalents, you'll need to do the translation on your own if you use yarn.

Not infrequently, I get reports from GitHub's security scanner, or while running npm install about security vulnerabilities in dependencies. These vulnerabilities rarely exist in packages that I depend directly on - more often they exist in packages that my dependencies depend upon themselves somehow. Here's a quick writeup of how I think about resolving these.

Make the robots fix it

In the best case, these vulnerabilities are immediately fixable by robots.

npm audit fix

a screenshot of the output of npm audit, showing fixable
vulnerabilities

In the case, you've been alerted to a security vulnerability while running npm install, which tells you that the vulnerability can be entirely mitigated by running npm audit fix. This command knows that the version of the vulnerable package that contains the patch is within the version range specified by the depending package. It's able to bump the vulnerable package in the package-lock.json, and the issue is solved.

GitHub Dependabot security alerts

github dependabot security alert showing two
vulnerabilities

Similarly, if you've received a security alert from GitHub Dependabot, you should be able to ask it to create a fix and send a pull request, as shown below.

a screenshot of a dependabot button to create a security
fix

Both cases above are examples of what to do when transitive dependencies contain vulnerabilities that are within the version range of the depending package. They'll also (from what I understand) check if the depending package has a later version with a version range that allows for the patched version of the vulnerable dependency, and upgrade that if necessary.

When the dependency can't be fixed by robots

But what if there is no version of the depending package that can use the patched version of the vulnerable package? At that stage, you have some decisions to make. You can either choose to put in the work of sending pull requests to each necessary dependency in the chain, and hoping that their maintainers will be responsive, or you can choose to ignore the vulnerability. This is a resource tradeoff, and the best decision will vary depending on your own context. Questions you should ask yourself include:

  • is my project actually affected by this vulnerability? - is the vulnerability in the execution path of my code? - is the vulnerability bundled into the package I distribute? - does the vulnerability affect developer tooling? - what is my organization's security policy? - does my organization provide resources for mitigating security risks like this? - do I have the time to dedicate to submitting one or more pull requests?

Whatever you decide: you have made the correct decision for your context. You are an expert in your domain, and you have weighed the pros and cons, sought out all the relevant information, and taken a calculated risk with either your engineering capacity or your risk budget.

Monitoring vulnerabilities

One trap to avoid, however, is conflating ignoring one specific vulnerability with ignoring them all. In a previous team, we had a script in our continuous integration pipeline that would get the output of npm audit, and fail the build unless there were no new vulnerability alerts. When a new vulnerability was found, all deploys were halted to prevent inadvertently deploying the vulnerability into production. Since the project was a monorepo used across 10+ teams, my team investigated any new vulnerability (transitive or non-transitive) immediately (within working hours), to avoid blocking other team's work. In general we followed the process above - if it were possible to get a robot to fix it, we would. Otherwise, we would weigh the effort of manually fixing against the severity of the vulnerability, and make an educated trade off. Of course, we did this within the context of our organization's security policies, which we didn't violate. If we decided not to fix the issue immediately, we added the advisory number to an allow-list of known issues, so that our script would know that we had accepted a given risk, and allow work (and deploys) to continue.

We also kept an eye on the vulnerabilities. On a regular schedule, we would audit each security advisory on our allow-list, and check to see if we had automatically mitigated it with dependabot, or if it had become worth the effort of fixing. We made sure not to let advisories needlessly languish on the list, and kept an eye out to make sure that we hadn't introduced the vulnerability into a new codepath, thereby changing the priority of the fix.

This never happened, but in extreme cases, if a security vulnerability was in danger of affecting our users, we would have forked the dependencies to mitigate the issue in the short term, and as a team come up with a strategy to idenfity a path forward - whether to continue maintaining our fork(s), find some way to switch to an alternative provider, or refactored the behaviour into some new structure, that we could consider releasing ourselves. Like I said though, this didn't happen while I was on that team, so this is purely hypothetical.

Update!!

My wonderful former colleague Drew who I miss a lot shared this tool for managing npm advisory allowlists in their projects! It looks excellent, and very in the spirit of what we used. Strong recommend!

Thanks!

for reading this post. If you have any questions or comments, or would like to berate me for doing something silly and wrong, please contact me on twitter or on mastodon, where I am available for all of the above.