Keep Your Ruby App Secure with Bundler
Tags: ruby, bundler
Published: 2023-07-05 13:12:52

This article covers the use of `bundler` features to secure Ruby applications. In this day and age, we have to be more and more careful about software supply chain security.

We'll show you how to start this journey by relying on a Gemfile and `bundler` to manage your project's dependencies.

By the end of the post, you will better understand how `bundler audit` and `bundler outdated` work. Both can help you monitor the security state of your project's dependency tree.

Let's dive in!

## An Introduction to Bundler for Ruby

The history of [Bundler](https://bundler.io/) is linked to [RubyGems](https://rubygems.org/). RubyGems, first released in 2004 by Chad Fowler, is a package manager that makes it possible to distribute and manage Ruby libraries, applications, and their dependencies.

Bundler, on the other hand, is a dependency manager. It was first released in 2009 by Carl Lerche.

Bundler helps developers manage dependencies between different Ruby libraries and applications. It uses a Gemfile to define the libraries and versions that a project depends on, then installs and loads those libraries in the correct order. Bundler provides a simple way to manage dependencies and ensure that all the required libraries are installed and loaded correctly.

## Supply Chain Security

RubyGems and Bundler can only solve some security issues. For example, in 2013, 2018, and 2020, several security issues emerged directly related to RubyGems. Each time, gems were compromised, potentially exposing thousands of projects and products to malicious actors.

Nowadays, the security of the software supply chain has become more high profile, due to major incidents like 2020's SolarWinds hack, EventStream in 2018, and Equifax, Maersk, Merck, and FedEx in 2017.

Let's see how Bundler can help us avoid some security issues.

## Bundler's Security-Related Features for Ruby

Bundler comes with several features that allow us to secure a Ruby project:

- **Source validation:** ensures gems are installed from a specific, trusted source.
- With **a dependency graph**, we can see a whole list of dependencies for each gem, helping us to identify potential security risks.
- **A `Gemfile.lock` file** records the specific versions of each gem used in the project. As it's packaged with the project, it ensures that the same versions of gems are used across different environments.
- **Vulnerability detection:** Thanks to an integration with the Ruby Advisory Database (RDB), Bundler can detect known security vulnerabilities in gems.
- **An audit command** lets you list known security vulnerabilities related to gems a project depends on.
- **Checksum verification:** Bundler verifies the checksum of each gem before installing it.
- **Gem signing and signature verification:** Gems can be signed cryptographically by a developer.

But, alas, signing gems is complicated and requires a lot of steps. As a consequence, not all gems are signed, thus putting many projects at risk.

Yet, in the meantime, we can rely on two features in particular to ensure that a project doesn't rely on packages that are too old or that have been identified as carrying a security risk.

These features are `bundler audit` and `bundler outdated`. Let's look at `bundler audit` first.

## About Bundler-audit

`bundle audit` is a command-line tool that helps you identify security vulnerabilities in the Ruby libraries that your project depends on. It is a plugin for the Ruby dependency manager Bundler and is included with Bundler version 1.10 and above.

`bundle audit` works by analyzing your Gemfile.lock file, which lists all the dependencies for your project and their versions. It then compares this information with a database of known vulnerabilities in Ruby gems (maintained by Rubysec).

If a vulnerability is found, `bundle audit` will provide you with information about the vulnerability, including its severity level and which gem versions have been affected. It will also suggest actions you can take to address the vulnerability, such as upgrading to a patched version of the gem or removing it entirely.

`bundle audit` is a useful tool for ensuring the security of your Ruby projects, especially if you are using third-party libraries or dependencies. By regularly running `bundle audit` as part of your development workflow, you can stay up-to-date on any vulnerabilities that may affect your project and take proactive steps to address them.

### Usage of Bundler-audit in Ruby

The basic use of Bundler-audit is through the `bundle audit` command:

```shell
$> bundle-audit

Name: actionpack
Version: 3.2.10
Advisory: OSVDB-91452
Criticality: Medium
URL: http://www.osvdb.org/show/osvdb/91452
Title: XSS vulnerability in sanitize_css in Action Pack
Solution: upgrade to ~> 2.3.18, ~> 3.1.12, >= 3.2.13

$>
```

As you can see, it outputs the name and version of a package used by the project we are testing, which contains a medium-level security issue. It also outputs the URL of the security advisory and a possible solution.

### Keeping Bundler-audit's Database Up to Date

As the audit is basically comparing a list of gems used in the project with a list of known security issues in a local copy of the security advisory database, you need to keep that database up to date.

To do so, you just need to run the `bundle audit update` command. This will fetch the latest version of the database. You should only have to run the audit again to check for any new security risks.

### Integration In a CI Pipeline

As with other tools aimed at keeping your project safe, it's best to ensure an audit runs automatically on a regular basis (if not after every commit and push to the Git repository).

You can rely on a git post commit hook to run the audit locally if the `Gemfile.lock` changes. Here is an example:

```shell
# .git/hooks/post-commit
if git diff --name-only HEAD~1 | grep -q Gemfile.lock; then
	echo "Gemfile.lock has changed, running audit..."
	bundle audit update && bundle audit
fi
```

This is a bit radical, but it works. You can rely on the same approach within the CI pipeline. As the `bundle audit` command will return a non-zero exit status, the CI pipeline will consider it to have failed. Unfortunately, we don't have a proper audit report out of the box. Instead, you have to direct the output of the `bundle audit` command towards a file and save it as an artifact of the build for the CI pipeline.

Here is how to direct the command's output to a file:

```shell
$> bundle audit update && bundle audit > /tmp/bundle-audit-report.txt
```

As each CI service handles artifacts differently, check the relevant documentation to see how to do this.

### Bundler-audit in Summary

`bundle audit` is not a complete solution, yet it still lets your team know if your project relies on unsafe gems (without costing you an arm and a leg). Your team should put tools such as git hooks or CI tasks in place, and relevant integrations to make any issues visible.

All things considered, I'd say it's not too much trouble. It probably requires a few hours to get started and have the basic information spit out as a comment in your favorite git hosting solution, or as a notice in a Slack channel.

Yet `bundle audit` is only one part of what you can do. It only tells you if there is a security issue related to a gem relied on by a project. It does not tell you if a gem is outdated. To handle that, `bundler outdated` is the command to use.

## Bundler Outdated for Ruby Gems

Auditing for security risks is very important, yet listing outdated gems is also a big issue. The longer you wait to update a dependency, the higher the risk that your code breaks.

You should aim to work in small iterations, deployed frequently, to limit the amount of change contained in each deployment. Equally, you should aim to update any gem you use as soon as it's updated, or as close to that as possible.

### Usage of Bundler Outdated

Just like `bundler audit`, `bundler outdated` is very simple to use. Just call it at the root of your Ruby project. It will compare the `Gemfile.lock` content to the current gem releases listed in it.

```shell
$> bundle outdated
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies......

Gem           Current    Latest    Requested   Groups
addressable    2.8.1      2.8.4
capybara       3.38.0     3.39.0    >= 0        test
devise         4.9.0      4.9.2     >= 4.6.0    default

$>
```

As you can see, three gems are outdated in this project. Let's see how we should read this table:

- The first column contains the name of the gem.
- The second column shows the currently installed version.
- The third column shows the latest version available.
- The fourth column shows the request version (from the Gemfile).
- The fifth column shows the group the gem is part of (in the Gemfile).

Here, we can see we don't have much to worry about for `addressable` and `devise`, as the difference in releases is within the patch versions (third part of the release number). The version of `capybara` is only behind one minor release, which implies a few more changes, but this rarely means breaking changes.

Still, the logical thing to do here is to update all three as soon as possible.

### Usage In a CI Pipeline

As `bundle outdated` will return a non-zero exit status for outdated gems, a CI task it's based on is considered failed. So you can have a simple and direct flag in your CI pipeline in case of outdated dependencies.

Yet you might want to rely on a pair of flags to improve your use of `bundle outdated`. The command can filter its output to only list gems that have pending patch-level updates:

```shell
$> bundle outdated --filter-patch
...
Gem           Current    Latest    Requested   Groups
addressable    2.8.1      2.8.4
devise         4.9.0      4.9.2     >= 4.6.0    default
```

In the same manner, you can list only gems with pending minor version updates:

```shell
$> bundle outdated --filter-minor
Gem           Current    Latest    Requested   Groups
capybara       3.38.0     3.39.0    >= 0        test
```

And in the same way, you can list only gems with pending major release updates:

```shell
$> bundle outdated --filter-major
Gem                Current    Latest    Requested   Groups
sidekiq-scheduler   4.0.3      5.0.1      >= 0       default
```

Those three flags let you easily tailor a CI task to only warn you that a patch-level update is available but not block the build (or the reverse), for example.

A complimentary option allows you to only list outdated gems within the default group:

```shell
$> bundle outdated --group default
...
Gem                Current  Latest  Requested          Groups
devise             4.9.0    4.9.2   >= 4.6.0           default
faker              3.1.1    3.2.0   >= 0               default
```

As gems within test and development groups don't impact the production release, they could be considered less of a priority to keep up to date, for example. Thus you can use the flag only to raise a warning or block the CI pipeline if gems within the default group are outdated.

## Wrapping Up

Maintaining the security of your Ruby application is not just the result of one action. Rather, it's a cumulative effect of several actions put together. As we've covered in this post, Bundler provides us with two commands that can help you to improve your application's security:

- `bundle audit` quickly lets you know if any gem you add or use in your project could pose a security threat and
  what to do about it.
- `bundle outdated` allows you to flag gems that should be updated. It complements `bundle audit`, and if you keep on top of it, you avoid having an update bundle that's too large.

These two commands are easy to use and integrate with local feedback loops and CI pipelines.

Happy coding!

**P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, [subscribe to our Ruby Magic newsletter and never miss a single post](https://blog.appsignal.com/ruby-magic)!**