Avoiding Dependency Confusion in Ruby with Bundler
If you have read about Alex Birsan’s recent “dependency confusion” attack and you use Ruby, you are probably wondering how to make sure you are not vulnerable to the same attack.
Fortunately it is easily avoided with Bundler, but there are some caveats.
TL;DR:
- Update to the latest Bundler.
- Don’t use global sources.
First of all, let’s look at the classic dependency confusion attack. This is possible when you declare your dependencies and their sources, but don’t tell Bundler which gems to load from which sources:
source 'https://rubygems.org'
source 'https://internal-gem-server.acme.corp' # DO NOT DO THIS
gem 'rails'
gem 'acme-logger'
In this scenario, the developer’s intention is to load rails from RubyGems.org, and “acme-logger” from Acme Corp’s internal gem server. However, because Bundler hasn’t been told to only fetch the “acme-logger” gem from the internal gem server, it will use the version on RubyGems.org if it has a higher version.
This leaves room for an attacker like Alex or myself to swing in and register “acme-logger” on Rubygems with high version numbers, so that your builds pick up our code instead.
This scenario is so dangerous that Bundler spits out a warning about it, and this configuration is even deprecated and wil
[DEPRECATED] Your Gemfile contains multiple primary sources. Using `source` more than once without a block is a security risk, and may result in installing unexpected gems. To resolve this warning, use a block to indicate which gems should come from the secondary source. To upgrade this warning to an error, run `bundle config set --local disable_multisource true`.
In my experience, this type of classic dependency injection is very rare to see with Bundler, probably because most examples and tutorials use the block form of source. However, if you want to prevent it entirely, you can disallow this behaviour:
$ bundle config set disable_multisource true
You can also set this via the environment variable BUNDLE_DISABLE_MULTISOURCE.
This might be a good idea to set in the build configuration for your production servers/images. Once this is set, trying to install the above Gemfile results in an error:
[!] There was an error evaluating `Gemfile`: This Gemfile contains multiple primary sources. Each source after the first must include a block to indicate which gems should come from that source. Bundler cannot continue.
So what is the correct way to specify your gems and their sources? The block form of source, which tells Bundler which gems to load from a particular source.
source 'https://rubygems.org'
gem 'rails'
source 'https://internal-gem-server.acme.corp' do
gem 'acme-logger'
end
With this Gemfile, Bundler will load rails from RubyGems.org and only RubyGems.org, and it will load “acme-logger” from the internal gem server and only from the internal gem server.
There is one absolutely massive caveat, however: Bundler will still load dependencies of “acme-logger” from either source! If “acme-logger” depends on “acme-util”, and someone uploads their own “acme-util” to RubyGems.org with a higher version number, on bundle update (or a fresh bundle install) Bundler will fetch the “acme-util” from RubyGems.org.
This bug was fixed in Bundler 2.2.10, so the fix is simple: make sure you are using Bundler 2.2.10 or greater everywhere in your organisation. This is a very recent version (released on February the 15th) so updating to the latest Bundler should be a priority right now for all Ruby shops using private gem repositories.
To recap:
- Use the block form of
sourceto specify which repository to load private dependencies from. - Consider setting
disable_multisource/BUNDLE_DISABLE_MULTISOURCEon production servers, build environments, images, etc. - Update to the latest Bundler (at least 2.2.10).
Gem install
One thing to note is that how you configure your Gemfile only affects Bundler, and anyone running gem install may still be vulnerable to dependency confusion. This is less of a concern for production environments, as you are hopefully only using Bundler to install gems there, but an attacker gaining access to an employee laptop still wouldn’t be great (and bear in mind that code can run on gem install, so a malicious gem only needs to be installed, not loaded and run).
A way to prevent this issue would be to use an internal gem repository that mirrors RubyGems.org for public dependencies, and block your internal gems from being loaded via that source. You then need to ensure all employee machines are using that gem repository, not just in Gemfiles, but as their global gem source as well.
For this to work, you will need a way to guarantee that every internal gem is on the blocklist. I have written another post on a robust approach to preventing dependency confusion attacks which details a bullet-proof scheme to ensure none slip through the net.
Do bear in mind, however, that the “gem install” issue is a less likely occurrence that is also lower impact, so your priority should be making sure your Gemfiles and Bundler configurations are safe first.
Mixing internal and public dependencies
Some organisations create a “virtual” repository (which provides a single URL to acess several different repositories) that combines a mirror of RubyGems.org and their internal gem server. In such an organisation, our example Gemfile might look like this:
source 'https://internal-combined-gem-server.acme.corp'
gem 'rails'
gem 'acme-logger'
The advantage is only having to specify one server to fetch gems from, I suppose.
I recommend against this approach in general, because the repository mirror doesn’t seem like the right place to determine which gems come from which repositories. That’s Bundlers job! Doing it on some remote server at a distance from the machine installing dependencies means your developers cannot see how those dependencies are being resolved.
Specific to our current concern, this means the job of avoiding avoiding dependency confusion attacks falls solely on your repository software rather than Bundler. You will need to check the documentation and configuration for your particular server, but it might not even be possible: at the time of writing, this bug in Artifactory is unresolved.
If you do mix public and private packages in a single repo, I would recommend splitting them apart instead. If that is not practical, or you need a stop-gap solution while you do that, make sure that all private gems are blocklisted from being loaded from public repositories in the configuration. You can do this in Artifactory with exclude patterns.
For this to work, you need to ensure your blocklist captures every single one of your private gems, past, present, and future. To avoid any slipping through, you may want to adopt the robust naming scheme I outline in my other post on dependency confusion attacks.