This is part 3 of a 3-part series on maintainable software. The other parts are linked below.

What is “maintainable software”?

In our definition, the primary characteristics of maintainable software are:

  • Developers new to the project can start working quickly.
  • Features can be added and changed efficiently.
  • Security patches can be applied quickly and easily.

This post focuses on the practices that enable quick and easy security patches.

Why is it important for security patching to be quick and easy?

The internet is a hostile place! There are essentially constant attacks on every piece of internet-connected software. When a new flaw in a common piece of software is discovered, exploits of that flaw can often be found the same day (AKA a “zero-day” vulnerability).

Before going further, let’s describe two classes of vulnerabilities.

  1. Application code can contain vulnerabilities, inadvertently created by your developers, that can result in malicious users being able to perform unwanted actions in your app.
  2. Dependencies (AKA software libraries used by your application) can also contain vulnerabilities. Because many software libraries are widely used, such a weakness can potentially affect thousands of applications.

Attacks exploiting the second type (vulnerabilities in dependencies) are much more common, because a successful attacker can gain unauthorized access to many applications (instead of just one). The reward of exploiting a dependency vulnerability is potentially much higher, so there are more malicious actors dedicated to that cause. It is also possible for such attacks to be automated, so your application can be affected even if the attacker is not directly targeting it. Some recent examples of dependency vulnerabilities include Heartbleed, Drupageddon, and ImageTragick.

Luckily, when we use open-source software, we are part of a community that can usually respond quickly to vulnerabilities. In fact, the announcement of a new vulnerability is often accompanied by instruction on how to patch it, usually by simply upgrading that dependency to a newly-released version.

The developers who can do that upgrade quickly are much less likely to be affected by the attacks that inevitably follow a vulnerability announcement.

What can I do, as a non-developer, to ensure that software patches can be applied quickly?

The age of dependencies can be a hard thing for non-techies to see, but there are some steps you can take to get a handle on this important aspect of your software.

  1. Ask your developers how often they upgrade dependencies. Answers can range from “daily” (a great answer) to “never” (a scary answer).
  2. Ask your developers to start measuring the age of their dependencies using a metric like libyear, developed by Singlebrook’s own Jared Beck.
  3. Loudly encourage dependency upgrades in your organization.
  4. Be tolerant when dependency upgrade work takes time away from feature development or results in bugs.
  5. Encourage automated testing! It’s the best way to find bugs created by dependency upgrades.

Are you worried about your ability to respond to vulnerabilities?

If you’re not sure about your organization’s ability to respond to newly-reported vulnerabilities, we’d love to hear from you. We have a lot of experience with upgrading dependencies and we’re passionate about the topic!

As a developer, what can I do to make security patching quick and easy?

Upgrade dependencies frequently

Upgrading your dependencies frequently (daily or weekly) is a very manageable task, often taking only a few minutes. By upgrading frequently, you can avoid encountering a tangled web of outdated dependencies, in which it becomes difficult to upgrade any of them due to lots of interdependencies on specific versions.

In our experience, the difficulty curve for dependency upgrades looks something like this:

As time goes by, and dependencies get more and more out of date, updating any particular dependency becomes more difficult. Generally, the latest releases of your dependencies will work well with each other, but old releases may not work well with new releases, and this incompatibility is often undocumented and under-constrained, so your package manager software may not be able to help you out much.

There can also be a small amount of extra work involved in upgrading to new, major-version releases right away. We call it the early- adopter penalty. That’s why the beginning of the chart above starts out a little higher and then dips down. You can sometimes save some effort if you wait for the X.0.1 release (instead of installing the X.0.0 release) because other people will have worked out the kinks in the new version. (If everyone did this, though, open-source software development would grind to a halt, so please do upgrade to that X.0.0 release and then report bugs and workarounds to the maintainers.)

Avoid version lock-in in your application’s dependencies

When listing your application’s dependencies in a Ruby Gemfile, a JavaScript package.json file, or another package file format, you can specify version constraints like “I need a 3.x release” or “I need version 2.1.3”, or you can choose not to constrain the version, usually by saying “*”, which means “any version is fine”.

One of the first things we do when starting a new Rails project is to remove all of the auto-generated version constraints from the Gemfile. We have found that unconstrained versions make it easiest for us to upgrade dependencies because we don’t have to edit the package file or even think too hard about which versions we want. We just run bundle update rails or yarn upgrade express and we get the latest version. Then, we rely on our test suite to tell us if the upgrade caused any problems that we need to fix. More on this in a moment.

Sometimes you need to constrain the version of a dependency, either because there are breaking changes that you’re not ready to deal with, or because you’re using a fork or a branch of the code and waiting for a pull request to get merged and released. When you do need a version constraint or fork or branch, you should add a comment describing why and list the conditions under which you can switch back to the latest release. (We are still looking for a great way to do this in package.json files, because their format disallows comments. If you’ve got this figured out, please let us know!)

Automated tests

As we alluded to above, your test suite needs to have your back and you should be able to rely on it to find issues caused by software updates. In the absence of a thorough, automated test suite, you’ll have to resort to testing your app’s features manually, which is time-consuming and error-prone. Unless you’ve got a dedicated QA team to do that testing, chances are good that manual testing will eventually get tiresome for developers or other stakeholders, and your users will end up doing your QA work after you release your changes to production.

Singlebrook is a big fan of automated testing. We’ve added test suites to apps built without them, improving their stability while enabling more frequent dependency upgrades.

Feeling stuck with your old dependencies?

If your application is running on out-of-date software and you need a plan to get current, contact us now and we’ll get you moving in the right direction!

If you missed them, Part 1 will help teach you to streamline developer onboarding, and Part 2 outlines some fundamentals of efficient development.