Skip to content
← Blog

Technical explainer

RubyGems supply chain security: native extensions and bundle install

3 min read

gem install and bundle install can run arbitrary code on your machine, and the mechanism predates npm's postinstall by years. It's called a native extension, and it's how a malicious gem gets code execution during a routine install.

This post covers the RubyGems install-time surface, what Gemfile.lock does (and what its newer CHECKSUMS section adds), and how a registry mirror intercepts gem downloads.

Native extensions are the install-time execution path

Many gems ship native extensions — C code compiled against the Ruby headers when the gem installs. The build is driven by an extconf.rb (or a Rakefile/Mkrf task) that RubyGems executes at install time. extconf.rb is ordinary Ruby; it runs with your user's privileges, with network and filesystem access, the moment you install the gem.

That makes it the Ruby equivalent of an npm lifecycle script: a legitimate feature (native gems need to compile) that doubles as a supply-chain foothold. A malicious gem doesn't need you to require it — installing it is enough.

What Gemfile.lock protects — and what CHECKSUMS adds

Gemfile.lock pins every gem (direct and transitive) to an exact version. For years that was reproducibility without integrity: the lockfile recorded what version, not a hash of the bytes.

Bundler 2.5.6+ added a CHECKSUMS section to Gemfile.lock: a SHA-256 of each gem's .gem file. When present, it lets Bundler — and any verifier — confirm that the gem you downloaded is byte-identical to the one that was locked. That closes the tamper-evidence gap, but only for projects that have opted into it; a lockfile without CHECKSUMS still verifies nothing about the bytes.

And as with every ecosystem: a checksum proves the bytes are the locked bytes. It does not prove they're safe. A typosquatted gem or a malicious new version locks just fine.

RubyGems is single-host — which makes gating simple

rubygems.org serves both the metadata (the compact index under /info/<gem> and /versions) and the .gem artifacts (/gems/<name>-<version>.gem) from one host. Unlike Cargo or Gradle, there's no second download host to route, so a single-upstream gate works.

Bundler also has a clean redirection mechanism: the mirror setting. Point bundle at a local gate as a mirror of rubygems.org, and every request — index and artifact — flows through the gate.

How install-time gating fits

With Bundler's mirror pointed at a local gate:

  • OSV lookup against the RubyGems ecosystem.
  • Threat-feed match for known-malicious gems.
  • SHA-256 verification of the downloaded .gem against the Gemfile.lock CHECKSUMS pin (when present).

Because extconf.rb is the real execution risk, the network gate pairs with an OS sandbox: the install — native-extension compile and all — runs contained, with filesystem access scoped to the project and the gem cache, and network restricted to the gate.

One caveat worth stating plainly: the plain gem CLI doesn't honor Bundler's mirror env, so install-time gating targets the bundle path (the lockfile-driven install most projects use). For ad-hoc gem install, the lockfile-scanning verifier is the right tool.

With Veln, veln safe bundle install sets Bundler's mirror to a local gate whose upstream is rubygems.org, scores every .gem (and verifies its CHECKSUMS hash when locked), and runs the install inside an OS sandbox. veln verify reads Gemfile.lock — including CHECKSUMS — for CI and pre-commit gating.

The practical takeaway

Upgrade Bundler and regenerate Gemfile.lock so it carries CHECKSUMS — that alone gives you real tamper-evidence for free. Then treat bundle install as code execution, because native extensions make it so. Checksums catch substitution; OSV and threat-feed catch known-bad; a contained install catches what a malicious extconf.rb would otherwise do unsupervised.