Deploy Node.js securely: Continuous update of dependencies

An important part of Node.js application life-cycle management is updating dependencies. Some developers have very strong opinions about this, occasionally forged in the fires of difficult debug sessions. Experience is a good thing, but if the wrong lessons are learned, experience can lead to cargo cult programming. In this article, I discuss common misconceptions related to dependency updates, when you should be updating your dependencies, and what tools to use to make updates.

Common misconceptions

Misconception: semver-minor is more risky than semver-patch

This is a common misconception. Even the traditional name of the semver parts — major, minor, and patch — can bolster this impression, which is one reason I am always careful to distinguish between minor changes (meaning small or unimportant) and semver-minor changes.

The most common symptom of this misconception in the context of dependency updates is package specifications like:

  "some-module": "~2.5.1",

instead of:

  "some-module": "^2.5.1",

Those package specifications imply that a semver-minor change is somehow more risky than a semver-patch change, but nothing about semver says anything about the “risk” of updates, only the “possible affect on users of the package”.

Semantic versioning review: The effect of ~2.5.1 is to only allow some-module to be updated to the latest in the 2.5.x version range. The effect of ^2.5.1 is to only allow some-module to be updated to the latest in the 2.x version range. Semantic versioning requires that if a backwards incompatible change is made to 2.5.1, that the version be updated to 3.0.0; if a feature is added to 2.5.1, that the version is updated to 2.6.0; and if a bug fix or refactor is made to 2.5.1, that the version is updated to 2.6.2. See the npm semver docs.

A common example of a semver-minor update is adding a new function or a new option to an existing function. Since this is, quite possibly, new code that users of pre-existing APIs won’t use, it is one of the safest updates (well, other than a change to the README!).

On the other hand, a bug fix (semver-patch) is by definition a change to the behavior of an existing API. Granted, it’s a change from a behavior someone thought problematic to one they like better, but it is a change that could cause problems if your application depended subtly on the previous buggy behavior.

A semver-major update is incompatible, but incompatible to what and to what degree? A careful maintainer describes a change that they know will break for an API user as semver-major, but, often, these changes are made reluctantly and only to corner cases or APIs that your application may not even use.

That’s not always the case, of course. It’s also possible that the API was completely rewritten! My point is that semverity alone does not distinguish between a complete rethink of the API and a tiny change to a corner case of an infrequently used API.

Limiting dependency updates to only semver-patch has the superficial appearance of only allowing bug fixes, but in practice has the opposite effect: preventing bug fixes and security updates.

Bug fixes of all sorts, including security fixes, are made by most projects as semver-patch (if possible) updates to their latest release, but not to previous semver-minor releases.

In theory, a project with a high profile or more active maintainers might backport a fix to previous semver-major release lines. Even though it’s possible with npm and allowed by semver, I can’t recall a single example of a project backporting a security patch to every single semver-minor point release.

Misconception: Updating frequently is riskier than infrequently

In general, Node.js packages can coexist at multiple version levels. This is a strength of Node.js’ module system and avoids the “dependency hell” that can occur in other languages. It also prevents us from delaying package updates indefinitely.

The only reason you must update a package is to get a new feature — or to get a fix for a security vulnerability or other bug. Hopefully, your QA has ensured that your application has no critical bugs affecting your customers, but security vulnerabilities almost always feel like they are reported at the worst possible times.

The more up-to-date your dependencies are, the more likely that a vulnerability fix is going to be a small change to a package version you are already running in production.

Updating across many versions of a package to get a security update is itself risky. It can surface all kinds of issues unrelated to the fix you want. Dependency updating is the kind of activity that your team should plan for and executed when it works for your team, probably done continuously in small increments, not forced on you because of a new vulnerability report.

Another problem with not keeping up to date with latest majors is that security vulnerabilities are often reported on transitive dependencies (dependencies of dependencies of dependencies of …). Even if that deep dependency released fixes on all its release lines, it’s entirely possible that one of the intermediate packages between you and the vulnerable package used a ~ dependency, effectively preventing the fix to become available to your package.

This is a difficult problem, because fixing it depends on convincing an open source maintainer to publish a new package version. Often the main line of dependencies the latest semver-majors will be fixed, but we frequently see that users on older release lines of their top-level packages don’t receive security fixes.

This is a major reason to as much as possible keep up with the latest majors. Even if you don’t need the updates, when you can’t get a security patch without first refactoring your code to a new API, you may regret having left this update to a time not of your choosing.

When to update: At a regular cadence

Dependencies should be updated at a regular cadence, during periods of low-risk. The beginning of a sprint or development iteration would be a good time. Right before a business critical update is shipped is a bad time.

What to update: Use tools to find out

Because teams have different workflows, I can’t be too prescriptive about which tools will work best for your team.

Tools fall into two broad categories:

  • web services that inform you when updates are available, possibly even PRing the updates and running your tests
  • command line tools

Of these tools, here are some that I’ve found most helpful when updating dependencies.

npm comes with two sub-commands that are useful:

Some packages try to improve on those, for example:

A number of services exist that attempt to automate the update process, but allow you to choose the timing of the update: