A definitive guide to Ruby gems dependency management

Liran Tal - Sep 29 '22 - - Dev Community

Ruby, much like other programming languages, has an entire ecosystem of third-party open source libraries which it refers to as gems , or sometimes Ruby gems. These gems are authored by the community, and are available from RubyGems.org which is the official registry for Ruby libraries. Similarly to other open source ecosystems, threat actors may publish deliberate malicious code or such which includes backdoors or credentials harvesting. Hence, attention to detail for how you manage and audit your open source Ruby gems is crucial.

In this article, I’ll run through the concepts and tooling that make up the Ruby dependencies ecosystem, and answer some of the common questions Ruby developers have.

The Ruby programming language

You might have heard about Ruby on Rails (RoR), the popular web development framework that contributed a lot to Ruby’s success and popularity. RoR is based on Ruby which is is a dynamic, cross-platform and interpreted based language. Its source code files are easily recognizable using the .rb file extension.

What are Ruby gems?

Gems are packaged source code libraries that are modular, independent and are easily reusable across projects.

What is RubyGems and how is it different from Bundler?

RubyGems is the central package registry where third-party, open source Ruby gems are shared as well as the official Ruby package manager, known as gem when interacted with via the command line interface.

RubyGems is a wonderful way to allow developers to tap into an ecosystem of hundreds of thousands of open source libraries and utilize them in their projects.

RubyGem’s own gems CLI comes pre-installed with a working Ruby environment so developers can get right away with installing their favorite open source dependencies. Such as:

sh
gem install rails
Enter fullscreen mode Exit fullscreen mode

The gems CLI, however, manages dependencies each on their own by fetching them from a registry and installing them on the filesystem so that they are available and easily imported to Ruby code projects.

The Bundler project, which is technically a Ruby gem itself and is installed as one, eases the burden of installing gems one-by-one, and allows for a deterministic and consistent dependency tree using a manifest file known as Gemfile and its counterpart lockfile Gemfile.lock. The latter pins the dependencies across the tree and ensures consistency across installations over time.

Since Bundler is a gem, getting started with Bundler is as easy as:

sh
gem install bundler
Enter fullscreen mode Exit fullscreen mode

This commands makes the bundler command line tool available to developers

The Ruby Gemfile

The file named Gemfile is a manifest text file that describes all dependencies that should be used by the project, and other metadata instructions that the bundler tool exposes.

A simple Gemfile is as follows:

ruby
​​source 'https://rubygems.org'

gem ‘rails’, ‘4.2.3’
gem ‘sqlite3’
Enter fullscreen mode Exit fullscreen mode

The above Gemfile defines the source entry which is the remote repository from which Bundler will download all the gems from, and each gem line defines an open source library, which version to fetch, as well as the source to find it at.

The following is a more complex example showing how Ruby Gems are sourced from different locations:

ruby

gem 'nokogiri'
gem 'nokogiri', git: 'https://github.com/sparklemotion/nokogiri.git', branch: main'
gem 'nokogiri', github: ‘sparklemotion/nokogiri’
gem 'nokogiri', path: '/path/to/local/nokogiri’
Enter fullscreen mode Exit fullscreen mode

More about these sources are described to length in the official Bundler git documentation.

What makes up a Ruby gem?

A Ruby gem is a packaged and modular Ruby source code that can be re-used by other Ruby gems, by Ruby applications, or generally by users interacting with it via the command line, like we’ve learned about Bundler earlier in this article.

Ruby gems usually follow a convention that includes the Ruby source code, tests, potential executable files made available to the user’s file path, and most importantly Ruby gems specification file, known as the Gemspec.

sh 
base64ness/
├─ lib/
│ ├─ base64.rb
├─ bin/
│ ├─ base64ness
│ ├─ index.html
│ ├─ robots.txt
├─ test/
│ ├─ base64
├─ base64ness.gemspec
├─ Rakefile
├─ Gemfile
├─ Gemfile.lock
├─ README.md
Enter fullscreen mode Exit fullscreen mode

Some of the directories like test/ and bin/ should be fairly straightforward as they represent the testing code with this dependency and the executable file to be shipped and installed with it. Other files we can call out are the Gemfile and Gemfile.lock files which we learned about when introducing Bundler to manage Ruby gems and its lockfile to make sure installs are deterministic and safely repeatable.

The Rakefile is a file that is attributed to the open source Rake project, which is a make-like build utility for Ruby projects. Thus, the name is similar to Makefile. Here’s an example to its contents:

ruby
require "rake/testtask"

Rake::TestTask.new do |t|
  t.libs << "test"
  t.verbose = true
  t.test_files = FileList["test/**/*.rb"]
end

desc "Run tests"
task default: :test
Enter fullscreen mode Exit fullscreen mode

The Rakefile imports the needed classes from the Rake gem and defines a test task. When the rake test command is executed in this directory it signals the Rake program to match using a glob pattern all the test files in the test/ folder and then run them. For more details on the format of the Rakefile refer to the Rake project repository’s documentation.

Another new file that you may have noticed in the file tree above is the gemspec file, which is often named the same as the package, such as base64ness.gemspec. The gemspec describes the Ruby gem and provides metadata for this dependency and is helpful to the RubyGems registry to track, analyze and make available this information to users browsing for Ruby gems.

Here’s an example of the above referenced base64 ruby gem base64ness.gemspec:

ruby
Gem::Specification.new do |s|
  s.name = "base64ness"
  s.version = "1.0.0"
  s.summary = "It does magic with base64!"
  s.description = "This Ruby gem adds extended utilities for base64 encoding and decoding tasks such as making them URL compliant."
  s.authors = ["Liran Tal"]
  s.email = "liran@snyk.io"
  s.files = ["lib/base64.rb"]
  s.homepage =
    "https://rubygems.org/gems/base64ness"
  s.license = "MIT"
end
Enter fullscreen mode Exit fullscreen mode

The RubyGems website provides a complete documentation for the Specification class we reference above, such as further metadata attached to a Ruby gem, extended capabilities allowing Ruby maintainers to define executables, bundling C extensions to Ruby gems which through cross-compilation provides native modules capabilities, and more.

Tip: if you’re curious about what’s inside a particular Ruby gem, you can unpack it to a directory and examine the source code as follows:

sh
gem unpack your-ruby-gem.gem --target /path/to/gem-directory
Enter fullscreen mode Exit fullscreen mode

Controlling Ruby gem dependency installation

Dependencies can be logically grouped into specific categories in which they relate. For example, you can have production Ruby gem dependencies, as well as development Ruby gems that you’d install only when developing locally.

To differentiate between them you can define the following Gemfile manifest:

ruby
group :production do
  gem ‘rails’,
end

group :development do
  gem ‘rspec-rails’
end
Enter fullscreen mode Exit fullscreen mode

You can then install dependencies by category with Bundler. For example, the below command would only install production dependencies:

sh
bundle install --without development
Enter fullscreen mode Exit fullscreen mode

Why do we need a Gemfile.lock?

If you were solely restricted to defining your dependencies using bundler’s Gemfile package manifest, then you’d be subject to the following constraints:

1) Only direct Ruby gems dependencies are documented.

2) These direct Ruby gem dependencies use a sparse and loosely defined version for the package. It could be fetching latest, or just the latest in a semantic version range.

3) Even if you pin these direct Ruby gem dependencies to hard-coded versions, they could still resolve an unexpected dependent gem version with every new install.

The above leads to the fact that without a lockfile to pin down the entire nested dependency of Ruby gems installed for your project, you’d be introducing indeterministic versions of installed gems. That’s probably the last thing you want happening in an automated CI or build environment, or for the other Ruby developers who collaborate on the project with you.

Therefore, I give you the Gemfile.lock:

ruby
GEM
  remote: https://rubygems.org/
  specs:
    actioncable (7.0.3.1)
      actionpack (= 7.0.3.1)
      activesupport (= 7.0.3.1)
      nio4r (~> 2.0)
      websocket-driver (>= 0.6.1)
    actionmailbox (7.0.3.1)
      actionpack (= 7.0.3.1)
      activejob (= 7.0.3.1)
      activerecord (= 7.0.3.1)
      activestorage (= 7.0.3.1)
      activesupport (= 7.0.3.1)
      mail (>= 2.7.1)
      net-imap
      net-pop
      net-smtp

PLATFORMS
  aarch64-linux

DEPENDENCIES
  bootsnap
  capybara
  debug
  importmap-rails
  jbuilder
  puma (~> 5.0)
  rails (~> 7.0.3, >= 7.0.3.1)
  rails_admin!
  selenium-webdriver
  sprockets-rails
  sqlite3 (~> 1.4)
  stimulus-rails
  turbo-rails
  tzinfo-data
  web-console
  webdrivers

RUBY VERSION
   ruby 3.1.2p20

BUNDLED WITH
   2.3.18
Enter fullscreen mode Exit fullscreen mode

Let’s review the specification and format of this gem dependency lockfile:

  • The GEM directive starts the block for listing out all the Ruby gem dependencies in a nested tree format which shows direct and transitive dependencies and their versions. These are identified under the specs directive. The remote directive instructs the bundler tool where is the source to fetch these Ruby gems for installation.
  • The PLATFORMS directive is an open list of target platforms for which building native Ruby gems is required, due to the cross-compilation chain needed for Ruby extensions written in C, for example.
  • The RUBY VERSION directive is optional and specifies the Ruby runtime version that was used when creating this Gemfile.lock lockfile.
  • The BUNDLED WITH directive specifies the version of Bundler which was used to create the lockfile. Developers which are using older versions should consider upgrading to stay up to date and maintain consistent and coherent Ruby gem install results.

bundler-audit and the case for improved Ruby gems security

With a great ecosystem, comes great responsibility. Indeed. This is where the community-powered project called bundler-audit comes in. It’s a Ruby gem which scans through the dependencies specified with the project’s lockfile (Gemfile.lock) and compares that with a database of vulnerable Ruby gems and will report any findings of versions that found matching one or more vulnerabilities.

Getting started with bundler-audit for Ruby gems vulnerability scanning

To begin scanning for vulnerabilities with bundler-audit, we first install it, just like any other Ruby gem:

sh
gem install bundler-audit
Enter fullscreen mode Exit fullscreen mode

Once installed, bundler-audit needs to download security advisories from resources such as the community-maintained ruby-adivsory-db, which itself sources data from GitHub Advisory and Google’s maintained Open Source Vulnerability database:

sh
bundler-audit update
Enter fullscreen mode Exit fullscreen mode

If successful, the bundler-audit command should output something similar to the following, indicating that everything is up to date:

sh
Updating ruby-advisory-db ...
hint: Pulling without specifying how to reconcile divergent branches is
hint: discouraged. You can squelch this message by running one of the following
hint: commands sometime before your next pull:
hint:
hint: git config pull.rebase false # merge (the default strategy)
hint: git config pull.rebase true # rebase
hint: git config pull.ff only # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
From https://github.com/rubysec/ruby-advisory-db
 * branch master -> FETCH_HEAD
Already up to date.
Updated ruby-advisory-db
ruby-advisory-db:
  advisories:   596 advisories
  last updated: 2022-07-18 13:57:38 -0700
  commit:   66f047bdcb4bbda76857f9ba668b7d71b641b28b
Enter fullscreen mode Exit fullscreen mode

Then we can run bundle-audit without any extra arguments and test for security vulnerabilities. Currently, for my local Ruby on Rails project, it printed the following:

No vulnerabilities found
Enter fullscreen mode Exit fullscreen mode

This looks good, but can be misleading and expose you to unnecessary risk. In fact, this is a false negative because security vulnerabilities do exist for some of the dependency versions that I have installed.

With all the respect attributed to the work done by the community to build security tooling, they’d probably be understaffed and not resourced well enough to have proper vulnerability coverage. This is where Snyk Open Source’s security scanner comes in.

Snyk has a (free) CLI tool, backed by a developer-first security company with a highly rich database of Ruby gems vulnerability reports, that you can install and scan your dependencies.

Scan your Ruby gem dependencies for free

Create a Snyk account today to access vulnerability reports for Ruby gems and thousands of other open source projects.

Sign up for free

Securing Ruby gems (for free) with Snyk

Let’s run Snyk for a test drive and see what security vulnerabilities it comes up with for our Rails project? If you’re on an M1 MacBook Pro like myself, run the following:

sh
curl https://static.snyksecdev.wpengine.com/cli/latest/snyk-linux-arm64 -o snyk
chmod +x snyk
mv snyk /usr/local/bin
Enter fullscreen mode Exit fullscreen mode

This will download the standalone Snyk binary, make it executable, and make it available in your path. Note, if you require other installation methods (such as brew for macOS, scoop for Windows, or otherwise, consult the getting started with the Snyk CLI docs.

Once we have the Snyk CLI installed, we can run it as follows:

sh
snyk test
Enter fullscreen mode Exit fullscreen mode

If this is your first time you’ll be greeted with the following message:

sh
`snyk` requires an authenticated account. Please run `snyk auth` and try again.
Enter fullscreen mode Exit fullscreen mode

Upon which you should type-in snyk auth and follow the instructions to copy an authentication URL on to the browser, login or sign-up action to receive your API key, and you’ll be back in your command line prompt in no-time to continue on with scanning:

sh
snyk test

Testing /home/app/myapp...

Tested 80 dependencies for known issues, found 2 issues, 59 vulnerable paths.

Issues with no direct upgrade or patch:
  ✗ Information Exposure [Medium Severity][https://security.snyksecdev.wpengine.com/vuln/SNYK-RUBY-ACTIONCABLE-20338] in actioncable@7.0.3.1
    introduced by rails@7.0.3.1 > actioncable@7.0.3.1 and 1 other path(s)
  No upgrade or patch available
  ✗ Web Cache Poisoning [Medium Severity][https://security.snyksecdev.wpengine.com/vuln/SNYK-RUBY-RACK-1061917] in rack@2.2.4
    introduced by capybara@3.37.1 > rack@2.2.4 and 56 other path(s)
  No upgrade or patch available

Organization: snyk-demo-567
Package manager: rubygems
Target file: Gemfile
Project name: myapp
Open source: no
Project path: /home/app/myapp
Licenses: enabled
Enter fullscreen mode Exit fullscreen mode

Well then, 2 issues and 59 vulnerable paths… not as safe as we thought we were. At the moment it looks like there are no newer versions to upgrade these libraries to across the nested dependency tree. So, to make sure Snyk monitors our project’s Gemfile continually to search for new version fixes to be applied we can do one of the following:

  1. Go to https://apps.snyksecdev.wpengine.com and connect Snyk to the source code repository on GitHub or elsewhere. This will allow Snyk to monitor this project in the background.
  2. Run the command snyk monitor which will take a snapshot of the current Gemfile and Gemfile.lock files and monitor them continuously. Be advised that you’ll need to run this from a CI or automated build so that every time those Ruby package manifest files are updated, you are sending a new snapshot for Snyk to monitor and keep track of.

Should I use bundle-audit or Snyk?

Use both. This isn’t a diplomatic statement, but rather they truly complement each other in the following ways:

  1. Snyk provides a rich database of security vulnerabilities in the Ruby ecosystem, manually curated and kept up to date, often even ahead of time of bundle-audit and others.
  2. Bundle-audit runs other checks aside from vulnerability scanning, such as verifying that your Ruby gems are not being fetched from insecure sources of the likes of http:// and git:// that would allow a man-in-the-middle attack, or lockfile injection attack, to tamper with the source code you receive and run.

Summary

We learned about managing Ruby gems as project dependencies, why using a lockfile is important, and outlined other aspects and recommended best practices, such as securely using Ruby dependencies by scanning them with tools like Snyk and bundler-audit.

Other Ruby resources:

For those of you who are also practicing JavaScript development, I highly recommend reading what is package lock json and how a lockfile works for yarn and npm packages.

Secure your Ruby gems with Snyk

Create an account today, and let Snyk's cutting edge security intelligence lock out vulnerabilities.

Sign up for free

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .