A definitive guide to Ruby gems dependency management
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.
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
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
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.
Gemfile is as follows:
ruby source 'https://rubygems.org' gem ‘rails’, ‘4.2.3’ gem ‘sqlite3’
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’
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
Some of the directories like
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.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.
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
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
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
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 = "firstname.lastname@example.org" s.files = ["lib/base64.rb"] s.homepage = "https://rubygems.org/gems/base64ness" s.license = "MIT" end
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
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
ruby group :production do gem ‘rails’, end group :development do gem ‘rspec-rails’ end
You can then install dependencies by category with Bundler. For example, the below command would only install production dependencies:
sh bundle install --without development
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
ruby GEM remote: https://rubygems.org/ specs: actioncable (220.127.116.11) actionpack (= 18.104.22.168) activesupport (= 22.214.171.124) nio4r (~> 2.0) websocket-driver (>= 0.6.1) actionmailbox (126.96.36.199) actionpack (= 188.8.131.52) activejob (= 184.108.40.206) activerecord (= 220.127.116.11) activestorage (= 18.104.22.168) activesupport (= 22.214.171.124) 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, >= 126.96.36.199) 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
Let’s review the specification and format of this gem dependency lockfile:
GEMdirective 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
remotedirective instructs the bundler tool where is the source to fetch these Ruby gems for installation.
PLATFORMSdirective 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.
RUBY VERSIONdirective is optional and specifies the Ruby runtime version that was used when creating this
BUNDLED WITHdirective 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
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
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
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
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.
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.snyk.io/cli/latest/snyk-linux-arm64 -o snyk chmod +x snyk mv snyk /usr/local/bin
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
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.
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.snyk.io/vuln/SNYK-RUBY-ACTIONCABLE-20338] in email@example.com introduced by firstname.lastname@example.org > email@example.com and 1 other path(s) No upgrade or patch available ✗ Web Cache Poisoning [Medium Severity][https://security.snyk.io/vuln/SNYK-RUBY-RACK-1061917] in firstname.lastname@example.org introduced by email@example.com > firstname.lastname@example.org 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
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:
- Go to https://apps.snyk.io and connect Snyk to the source code repository on GitHub or elsewhere. This will allow Snyk to monitor this project in the background.
- Run the command
snyk monitorwhich will take a snapshot of the current
Gemfile.lockfiles 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:
- 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.
- 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
git://that would allow a man-in-the-middle attack, or lockfile injection attack, to tamper with the source code you receive and run.
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:
Secure your Ruby gems with Snyk
Create an account today, and let Snyk's cutting edge security intelligence lock out vulnerabilities.