What to do with bundler-audit's output (after the panic)

Rob Bazinet, founder of RailsHealth.

By Rob Bazinet

· 6 min read

You ran bundler-audit check --update, and there are fourteen advisories. Or twenty-two. Or in the worst case I’ve personally seen on a real client app, twenty-nine.

The first move is not to update everything. The first move is to read each advisory and decide whether it matters to your app, in the specific way it’s deployed, in the specific way you use that gem.

This is the triage I do. It’s not technically novel — anyone who has done application security for a while runs roughly this process — but I find that working Rails developers, who are not security people by trade, tend to either panic (and burn a week updating dependencies they didn’t need to update) or shrug (and quietly ship code with a known CVE for a year). Neither one is the right answer.

Read the advisory, not the score

bundler-audit reports a “criticality” for each advisory: low, medium, high, critical. That’s the gem-ecosystem default, usually pulled from the CVE database via rubysec/ruby-advisory-db. It’s a useful sort order. It is not a priority list for your app.

Critical advisories in gems you don’t use the vulnerable part of are not critical to you. Medium advisories in gems that handle every request in your app are not medium to you. The score is calibrated to “this gem is used in some application, somewhere”; you have to recalibrate to “this gem, used the way I’m using it, in my application.”

For each advisory, I write a one-line note answering one question: is the vulnerable code path actually reachable in our app? That’s the triage.

The four buckets

After I read each advisory, every one falls into one of these:

1. Reachable and exploitable. The CVE describes behavior in the gem that our app actually triggers — we use that method, that endpoint, that parsing path. This goes to the top of the list and gets fixed now. “Now” usually means today.

2. Reachable but not exploitable in our deployment. The vulnerable behavior is in code we run, but the conditions for exploitation don’t apply — e.g., the CVE requires an authenticated user with admin role and we don’t expose that path to authenticated users at all, or the CVE requires a specific header we don’t accept upstream. Fix soon, but not today. Document why it’s not today.

3. Not reachable. The vulnerability is in a part of the gem we don’t load, don’t call, don’t use. Example: a Nokogiri XPath injection CVE when we only use Nokogiri for HTML parsing and never pass user input to XPath. Note it, ignore it, but re-check on any major version bump.

4. Indirect dependency, you don’t control the version. Some other gem you depend on requires this version. You can sometimes fix this with a Gemfile constraint or a bundle update of the parent gem. Sometimes you can’t, and the path forward is to update the parent or drop the parent.

The “update everything” anti-pattern

Don’t run bundle update to clear a bundler-audit report. You’ll pull in a new round of dependency updates across your whole tree, most of which are unrelated to the CVE you’re trying to fix, and now any bug you encounter in the next two weeks could be one of dozens of new versions.

Instead: bundle update <specific-gem> --conservative. That bumps the one gem and minimally adjusts the rest of the tree to make the bundle resolve. You get the CVE fix and ~zero collateral changes.

If --conservative won’t resolve, you’re in a transitive-dependency knot. Read the conflict, identify the parent gem that’s pinning the vulnerable version, and decide whether to bump that one too.

When there’s no patch available

Sometimes the gem maintainer hasn’t released a fix. Less often, the maintainer is gone — the gem hasn’t had a commit in three years, the GitHub repo says “looking for new maintainer,” and the advisory’s “patched versions” column is empty.

Your options, roughly in order:

  1. Pin to a known-good fork that has the fix. This is common with abandoned gems; somebody has usually forked it. Make sure the fork is a real fork from a real person — check the commit history, check the GitHub user, check whether it’s been merged anywhere reputable.
  2. Pin to your own fork with the fix backported. This is more work but more sustainable; you control the fork.
  3. Add a config or middleware-level mitigation that makes the vulnerability unreachable. For example, a rack-attack rule that blocks the malicious request shape, or an explicit input validation upstream of the vulnerable code path.
  4. Remove the gem. If it’s abandoned and there’s no fix and you don’t strictly need it, the cleanest fix is bundle remove <gem>.
  5. Document and accept the risk, with a calendar reminder to revisit in 30 days. This is the option of last resort. Do it explicitly; don’t do it by accident.

A note on Rails CVEs specifically

If bundler-audit reports a Rails-itself CVE, the math is different. Rails CVEs almost always have a patched version available — sometimes within hours. The fix is usually bundle update rails --conservative and then test. The exception is if you’re on an unsupported Rails major (5.x and earlier as of 2026), in which case the patch isn’t backported and your options narrow to: pay for Rails LTS, upgrade Rails, or accept the risk. Almost always: upgrade Rails. Which gets you into a separate planning exercise.

The output you actually want

After triage, I want a list that looks like:

CVE-2025-xxxxx rack high FIX NOW CVE-2024-xxxxx nokogiri critical not reachable (HTML-only) CVE-2024-xxxxx loofah medium FIX NEXT SPRINT CVE-2023-xxxxx some-gem low documented; revisit 2026-06-01

Four columns. Each advisory gets a decision. Anything that says “FIX NOW” goes in a branch today. Anything that says “FIX NEXT SPRINT” goes in a ticket. Anything that says “documented” goes in a markdown file somewhere your team can find it. Anything that says “not reachable” gets re-evaluated whenever you do a major gem version bump.

That’s the work. It’s straightforward, it’s tedious, and it’s exactly the kind of thing RailsHealth re-runs every week against your repo so you don’t have to remember to do it manually. The triage decisions still belong to you — only the discovery is the program’s problem.

Try RailsHealth

14-day trial. No credit card to start. Read-only GitHub access — we never run your code.

Connect your GitHub repo

Related guides

Feedback