A Gemfile lists what your Rails or Sinatra app depends on. A Gemfile.lock pins the exact versions Bundler resolved — and that lock file, not the Gemfile, is what actually ships to production. In July 2019, an attacker took over the RubyGems.org account behind the popular rest-client gem and pushed versions 1.6.10 through 1.6.13 laced with code that read environment variables and cookies and sent them to a remote server. The same week, the strong_password gem was hit the same way. No application changed a single line of its own code — bundle install just pulled in the compromise. RubyGems.org now hosts more than 185,000 gems, most of them pulled in as transitive dependencies nobody reads line by line. Real Gemfile security means treating that lock file as a live attack surface: scanning it for known CVEs, understanding how malicious versions get in, and gating merges before a bad release ships.
Why is the Gemfile.lock the real attack surface, not the Gemfile?
Because the Gemfile.lock, not the Gemfile, determines exactly which gem versions — including every transitive dependency — get installed on bundle install, and most of those entries were never chosen deliberately by anyone on the team. A Gemfile might declare gem "rails", "~> 7.1" and a dozen other top-level gems, but a stock rails new project on Rails 7.1 resolves to well over 130 gems in Gemfile.lock once ActiveSupport, ActionView, Nokogiri, Puma, and their own dependencies are pulled in. Developers typically write and review fewer than 15 of those entries directly; the other 90%+ arrive as second- and third-order dependencies chosen by gem authors, not by your team. Each one is code that runs in your process with the same privileges as your application. That asymmetry — a handful of reviewed lines controlling hundreds of unreviewed ones — is why supply chain security tools focus on the lock file, not the Gemfile.
How do you detect known-vulnerable gems already sitting in your Gemfile.lock?
You cross-reference every pinned version in Gemfile.lock against a maintained vulnerability database, which is exactly what bundler-audit does against the community-run ruby-advisory-db. Running bundle audit check --update pulls the latest advisory data and flags any locked gem version with a published CVE or GHSA. This isn't hypothetical: CVE-2019-5477 affected Nokogiri versions before 1.10.4 due to an XXE issue in a bundled libxml2, and it sat in thousands of Gemfile.lock files for months because Nokogiri is a transitive dependency of Rails itself, not something most teams update proactively. Going further back, CVE-2013-0156 was a YAML deserialization flaw in Rails' XML parameter parsing that allowed unauthenticated remote code execution — it was disclosed on January 8, 2013, and mass-exploited within days because so many production Gemfile.lock files still pointed at unpatched Rails versions. A quarterly bundle audit run is not fast enough for a vulnerability class that gets weaponized in under a week.
How did attackers actually get malicious code into gems people already trusted?
By compromising a maintainer's RubyGems.org account credentials and pushing a new, malicious version under an existing, already-trusted gem name — no typosquat required. In the rest-client incident, the attacker gained publish access to the account and released versions 1.6.10, 1.6.11, 1.6.12, and 1.6.13 between roughly July 19 and August 14, 2019, each containing code that exfiltrated ENV variables and cookie data. Because rest-client was a long-standing, widely trusted gem, bundle update on any app with a loose version constraint (gem "rest-client", "~> 1.6") would happily pull the poisoned release. The community caught it when a security researcher noticed a base64-decoded HTTP request going to an unfamiliar domain during a diff review, and RubyGems.org yanked the versions roughly 36 hours after the first report — but that window is long enough for a CI pipeline running bundle install daily to have already shipped it. The lesson for defenders: trust in a gem's install history is not the same as trust in its next release.
How can a look-alike gem name slip into your Gemfile without anyone noticing?
It happens because RubyGems.org allows near-identical names — including hyphen/underscore variants — to be registered as completely separate packages, and Bundler will install whichever one you typed. net-ssh and a hypothetical net_ssh are not the same gem to RubyGems, even though they read the same in a code review or a Slack message pasting an install command. A developer copying gem install instructions from a blog post, a stale README, or an AI-generated snippet can pull in a package that has never been audited, has a handful of downloads, and was published last week — while believing they installed the well-known library. This is the same technique that has repeatedly hit npm and PyPI, and RubyGems has no automated blocking for it today; the only real defense is verifying gem provenance (download counts, maintainer history, first-release date) before adding a new line to the Gemfile, not after bundle install succeeds.
How do you pin and verify gem versions so a bad release can't slip in silently?
You commit Gemfile.lock to version control, avoid floating constraints, and use Bundler's built-in checksum verification instead of trusting version numbers alone. Bundler 2.5, released in December 2023, added per-gem checksum entries directly in Gemfile.lock, so bundle install can detect if a package's contents changed without a version bump — closing exactly the gap the rest-client incident exploited. Pair that with bundle lock --local in CI to guarantee the resolved graph doesn't shift between environments, and avoid unpinned git: or path: sources in production Gemfiles, since a Git ref without a fixed SHA can change contents after your last install with no lock file diff to catch it. Gem signing exists in RubyGems (gem cert and -P HighSecurity) but adoption remains low — most published gems still ship unsigned — so checksum pinning in the lock file is currently the more reliable control most teams actually have available.
How do you stop a newly disclosed CVE from reaching production through CI/CD?
You add an automated dependency-audit gate to the pipeline that fails the build on new advisories, rather than relying on someone remembering to run bundle audit manually. When CVE-2022-32224 was disclosed on June 15, 2022 — a Rails Active Record vulnerability allowing remote code execution via crafted YAML during deserialization of certain database column types — the fix window mattered: any app still deploying from a Gemfile.lock pinned to the vulnerable Rails patch version was exploitable the moment the advisory and proof-of-concept went public. Tools like Dependabot or bundler-audit wired into a pre-merge CI check catch this automatically, but they only cover known, published CVEs against your locked versions. They don't tell you whether the vulnerable code path is actually reachable from your application's own code, which is the difference between a finding you can triage in five minutes and one that requires an emergency patch tonight.
How Safeguard Helps
Safeguard ingests your Gemfile.lock and generates or accepts an SBOM to build a complete, versioned map of every direct and transitive gem in your Ruby application — no guessing at what's actually installed. From there, reachability analysis determines whether a flagged CVE, like a Nokogiri XXE or a Rails deserialization bug, is actually exercised by your application's call paths, so security and engineering teams stop triaging vulnerabilities that can never fire in production. Griffin AI correlates that reachability data with exploit intelligence and package provenance to separate real supply chain risk — a hijacked maintainer account, a suspicious new release, a look-alike gem name — from routine version churn. When a fix is available, Safeguard opens an auto-fix pull request with the corrected Gemfile.lock entry and the checksum already verified, so the gap between disclosure and remediation is measured in minutes of review, not weeks of manual bundle updates.