Application Security

Reachability Analysis for Ruby and RubyGems in 2026

Ruby reachability under metaprogramming, Rails autoloading, and Bundler groups. What bundler-audit and modern tools handle, and where they punt to over-approximation.

Aisha Rahman
Senior Researcher
5 min read

Ruby is the language where reachability analysis is theoretically hardest and operationally most necessary. Rails monkey-patches the standard library at startup, ActiveRecord generates methods at runtime, and the language's method_missing pattern means a static call graph is always an approximation. Yet the Ruby ecosystem has a healthy population of large production codebases, and the SCA noise problem on a typical Rails monolith is bad enough that even a modest reachability filter delivers meaningful value.

This post covers what reachability looks like for Ruby in 2026, how the popular tools handle Rails-specific patterns, and the false-positive reduction numbers you can realistically expect. It is written for engineering teams running mid-to-large Rails monoliths who are tired of triaging CVEs that never touch their application.

Why is Ruby reachability harder than other dynamic languages?

Ruby's metaprogramming is more pervasive than Python's. ActiveRecord defines model methods based on database schema at startup, so a method like User.find_by_email does not appear in source until the model is loaded against the schema. ActiveSupport monkey-patches core classes with hundreds of additional methods. Concerns and mixins introduce methods through include and extend calls that static tools have to resolve transitively.

The pragmatic effect is that a Ruby call graph built without knowledge of Rails will miss 30 to 50% of real edges. Tools that handle Rails specifically, by parsing schema files and modeling ActiveSupport patches, recover most of that gap. CodeQL has limited Ruby support; Semgrep handles Ruby reasonably well for pattern-based queries but does not build a full call graph; Snyk Code and Endor Labs both ship Ruby-specific reachability that is usable in production as of 2026, though precision is meaningfully below their Java or Go results.

How do Bundler groups affect what is actually reachable?

Bundler's group mechanism lets a Gemfile declare gems that are only installed in specific environments: :test, :development, :production, and custom groups. A gem in the :test group with a known CVE is unreachable in production by definition, but a reachability tool that scans the full Gemfile without group filtering will report it as live. This is the single most common source of false positives we see on Rails services.

The right approach is to read Gemfile.lock together with the production group filter and reachability-analyze only the gems that are actually installed in the deployed bundle. Tools that consume bundle install --deployment --without development test output get this right; tools that parse the raw Gemfile do not. The recent additions to bundler-audit and the work in gem-compat projects have made this easier, but it still requires explicit configuration in most SCA platforms. Always verify your tool is honoring group filters before trusting its alert volume.

What about Rails autoloading and Zeitwerk?

Zeitwerk, the autoloader that became default in Rails 6, loads classes on first reference rather than at boot. This means the runtime call graph depends on which routes were hit during the analysis window. A static reachability analyzer that walks from config/routes.rb through controller actions captures the realistic entry points and traces forward from there. The challenge is that some classes are only loaded by background jobs or rake tasks, and these need to be added as additional entry points explicitly.

The pattern that works is to treat routes, Sidekiq workers, ActiveJob classes, and rake tasks as the four canonical entry-point types for a Rails service. A reachability tool that consumes all four produces results within a few percent of the true runtime call graph for most Rails monoliths. Tools that consume only routes will under-report, which produces false negatives, which is worse than the false positives because engineers stop trusting clean reports.

What does a real Ruby CVE look like with reachability?

Take CVE-2024-26143, the Rails ActionDispatch::Static path traversal issue. The vulnerable code is reached only when an application serves static files through ActionDispatch::Static, which is uncommon in production where nginx or a CDN typically handles statics. Reachability analysis correctly downgrades this on most production Rails services. We measured about 18% of audited Rails apps as actually reachable for this CVE.

Compare with CVE-2024-27282, the strscan ReDoS issue. The vulnerable scanner is used pervasively in Rails internals, including in routing and parameter parsing, which means it is reachable on every Rails service. Reachability provides no filtering benefit and the right response is to upgrade Ruby or strscan directly. The split in the Ruby ecosystem is similar to JavaScript: roughly 70% of gem CVEs are filterable through reachability, 30% are not.

What false-positive reduction is realistic for Ruby?

For a typical Rails service, reachability filtering reduces CVE alert volume by 55 to 70%. This is lower than Java or Go because Ruby's metaprogramming creates more genuine uncertainty in the call graph, and conservative tools have to over-approximate. The remaining alerts are higher quality and more likely to represent real risk, which is the operational outcome you are actually buying.

These numbers come from internal data on roughly 300 Rails services in mid-size SaaS environments that adopted reachability filtering during 2024 and 2025. The teams that benefited most were those running long-lived Rails monoliths with extensive gem trees; greenfield Rails services with tight dependency budgets saw smaller absolute reductions because their starting noise was lower.

How Safeguard Helps

Safeguard analyzes Ruby services with first-class modeling of Rails, Sidekiq, and ActiveJob entry points, and consumes Gemfile.lock with production group filtering applied. Griffin AI traces reachability through ActiveRecord-generated methods and ActiveSupport monkey-patches, explaining each finding in terms the engineer can act on. SBOMs cover both gems and native extensions, and policy gates can block deployment on reachable-critical CVEs in the production group while letting test-only findings flow through as informational. TPRM scores gem maintainers on response history and namespace ownership, and zero-CVE Ruby base images give Rails services a clean foundation for compliance audits.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.