If your Rails app has been in production for more than a few months, its Gemfile.lock is quietly accumulating risk. Every gem you depend on pulls in transitive gems you never explicitly chose, and any one of them can ship a disclosed CVE — a SQL injection in an ORM extension, an XXE in an XML parser, a deserialization bug in a background job library. A proper rails gemfile dependency audit is how you find those issues before an attacker or a pentest report does. This guide walks through auditing your Gemfile and Gemfile.lock end to end: inventorying what you actually run, scanning it with bundler-audit, triaging findings by real exploitability, patching safely, and wiring the whole thing into CI so it never goes stale again. By the end, you'll have a repeatable process, not a one-time scan.
Step 1: Inventory What's Actually in Your Gemfile.lock
Before you scan anything, understand what you're scanning. Gemfile lists your direct dependencies; Gemfile.lock is the resolved dependency graph, including every transitive gem and its exact pinned version. Most vulnerabilities show up in gems you never directly required.
Start by generating a full list of resolved versions:
bundle list --name-only > gems.txt
bundle outdated --strict
bundle outdated tells you how far behind each gem is from its latest release — useful context, but not a vulnerability signal on its own. A gem can be five majors behind and perfectly safe, or one patch behind and carrying a critical CVE. That's why version freshness and security auditing are separate concerns, and why the next step matters more than this one.
Also check for any gems pulled from git sources or path references instead of RubyGems:
grep -E "git:|path:" Gemfile
Gems sourced from arbitrary git branches or forks bypass RubyGems' publishing checks entirely and are invisible to most vulnerability databases — flag these for manual review since automated scanners can't see into them.
Step 2: Run a Rails Gemfile Dependency Audit with bundler-audit
This is the core of the workflow. bundler-audit rails projects almost universally standardize on the bundler-audit gem, which checks your Gemfile.lock against the Ruby Advisory Database, a community-maintained feed of known Ruby and Rails CVEs.
Install it and update its local advisory database:
gem install bundler-audit
bundle-audit update
Then run the audit from your Rails app root:
bundle-audit check --update
The --update flag refreshes the advisory database inline so you're never scanning against stale CVE data. A clean run looks like:
No vulnerabilities found
A finding looks like this:
Name: nokogiri
Version: 1.13.6
Advisory: CVE-2022-24836
Criticality: High
URL: https://github.com/advisories/GHSA-2qc6-mcvw-92cw
Title: Nokogiri Command Injection Vulnerability
Solution: upgrade to ~> 1.13.8
Each entry gives you the affected gem, the exact vulnerable version, a CVE or GHSA identifier, a severity rating, and — critically — the minimum safe version to upgrade to. Run this on a schedule, not just once; the Ruby Advisory Database is updated continuously as new CVEs are disclosed against gems already sitting in your lockfile.
Step 3: Cross-Check with bundle audit's Sibling Tools
bundler-audit is excellent for known-CVE matching but doesn't catch everything — it won't flag typosquatted gem names, unmaintained gems with no CVE filed yet, or license conflicts. Layer in a couple of complementary checks as part of your ruby on rails dependency scanning routine:
# Check for gems with no recent releases or maintainer activity
bundle exec ruby -e '
Bundler.load.specs.each do |spec|
puts spec.name if spec.date && spec.date < (Time.now - 3*365*24*60*60)
end'
If you use GitHub, enabling Dependabot alerts on the repository gives you a second, independent data source that cross-references GitHub's own advisory database — useful because GHSA and Ruby Advisory DB entries don't always land at the same time. Treat bundler-audit as your authoritative local scan and GitHub alerts as a backstop, not the other way around, since CI-based scanning is reproducible and works even for private forks GitHub can't see into.
Step 4: Triage Findings by Exploitability, Not Just Severity
Don't patch in CVSS-score order. A "critical" vulnerability in a gem that's only loaded in your Rakefile at build time is lower priority than a "medium" one in a gem that parses untrusted user input on every request. For each finding, ask:
- Is the vulnerable code path actually reachable from user input in this app?
- Is the gem loaded in production, or only in
development/testgroups? - Does the advisory require a specific configuration (e.g., a non-default parser mode) that you don't use?
Check which group a gem lives in:
bundle list --only-group production
A vulnerable rspec-mocks transitive dependency that only loads under group :test in your Gemfile is real technical debt but not an active production risk — bump it, but it shouldn't block a release the way a production-facing rack or actionpack CVE should.
Step 5: Patch and Re-Verify
For most findings, the fix is a version bump. Target the specific gem rather than running a blanket bundle update, which can introduce unrelated breaking changes right when you're trying to ship a security fix:
bundle update nokogiri --conservative
--conservative restricts the update to the named gem and its strictly required dependents, leaving the rest of your lockfile untouched. After updating, re-run the full test suite and the audit again:
bundle exec rspec
bundle-audit check --update
If the vulnerable gem is a transitive dependency with no direct upgrade path (your Gemfile doesn't reference it directly), check what's pulling it in:
bundle list --paths | grep nokogiri
bundle exec bundle-viz # if installed, or use `bundle exec gem dependency <gem>`
Then bump the parent gem that depends on it, or add an explicit version constraint in your Gemfile to force resolution to a patched version, provided the whole graph still resolves:
gem "nokogiri", ">= 1.13.8"
Step 6: Automate the Audit in CI
A rails gemfile dependency audit that runs once, manually, before a big release is better than nothing — but it misses every CVE disclosed the following week against gems you already shipped. Wire bundler-audit into your CI pipeline so every push and every scheduled run checks freshly.
Example GitHub Actions job:
name: Dependency Audit
on:
push:
schedule:
- cron: "0 6 * * *"
jobs:
bundler-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Install bundler-audit
run: gem install bundler-audit
- name: Run audit
run: bundle-audit check --update
The daily cron matters as much as the push trigger — most of the risk here isn't code you just wrote, it's a CVE disclosed today against a gem you added six months ago. Fail the build on any unignored finding, and use bundle-audit check --update --exclude-ignored alongside a documented .bundler-audit.yml allowlist for any advisories you've formally accepted and can justify in review.
Troubleshooting and Verification
bundle-audit reports "No vulnerabilities found" but you know a gem is old. bundler-audit only flags gems with a published advisory in the Ruby Advisory Database. An outdated gem isn't automatically a vulnerable one — cross-check with bundle outdated separately, and don't treat a clean audit as a freshness guarantee.
The advisory database update fails in CI with a network or git error. bundle-audit update clones the advisory database via git. In locked-down CI runners, allowlist github.com/rubysec/ruby-advisory-db or vendor a cached copy of the database and point bundle-audit at it with --database.
A finding shows up for a gem you can't upgrade due to a Rails version constraint. Document it. Add a scoped, time-boxed entry to .bundler-audit.yml under ignore: with the CVE ID, and set a reminder to revisit it — don't leave a bare ignore rule with no expiration or justification, since that's exactly the kind of finding an auditor or future teammate will flag as unmanaged risk.
Verify your pipeline is actually catching things by temporarily pinning a gem to a known-vulnerable version in a throwaway branch and confirming CI fails as expected. This closes the loop on whether your rails supply chain security tooling is wired correctly, rather than just assuming it is.
How Safeguard Helps
Running bundle-audit check in CI is a solid baseline, but it only tells you what's vulnerable — not what's exploitable in your specific deployment, what else in your build pipeline touches that gem, or whether the fix you shipped actually took effect in the artifact that reached production. Safeguard extends this workflow across your full software supply chain: continuous SCA against your Gemfile and Gemfile.lock correlated with reachability analysis, so you're not drowning in transitive-dependency noise from gems your app never actually calls into.
Safeguard also tracks provenance from commit to build to deployed artifact, so when a new CVE lands against a gem already in your lockfile, you get an alert scoped to exactly which services and environments are affected — not a generic advisory you have to manually map to your infrastructure. For teams under SOC 2 or similar compliance obligations, that audit trail (who found it, when, what was done, and evidence it shipped) is generated automatically instead of assembled by hand before every audit cycle. If bundler-audit in CI is your first line of defense, Safeguard is the layer that makes sure nothing that matters slips past it.