Best Practices

Enterprise Rails Security Audit: 2025 Field Notes

After 14 Rails audits in the last 12 months, the same eight issues kept surfacing. Here's the 2025 field checklist for Rails 7.2 and 8.0 enterprise apps.

Nayan Dey
Senior Security Engineer
5 min read

Ruby on Rails 8.0 shipped November 7, 2024, bringing Kamal 2 deploys, Solid Queue, and Propshaft as the default asset pipeline. Rails 7.2 remains the LTS most large enterprises run in production, and the two branches now carry a combined ~8 million GitHub stars' worth of downstream influence. Between April 2024 and April 2025, my team completed 14 Rails security audits across fintech, healthcare, and SaaS logistics. The findings were unsettlingly consistent: the same eight classes of vulnerability accounted for roughly 80% of High and Critical issues. None were novel. All were preventable with disciplined use of Rails' built-in primitives. This is the field-tested checklist we now run on every audit, grounded in CVEs and real incident data from the past year.

Are Strong Parameters actually being used correctly?

Usually not. In 10 of 14 audits we found at least one controller using params.permit! (bang) or permit(:everything_you_can_think_of) with sensitive attributes like role, admin, tenant_id. The 2024 Zendesk Strong Params bypass (Zendesk advisory ZEN-2024-03) traced back to exactly this pattern — a controller permitted organization_id, which allowed horizontal privilege escalation across tenants. Rails 7.1+ ships config.action_controller.raise_on_open_redirects = true; pair that with a CI lint rule that fails on permit! in controllers under app/controllers/api:

# .rubocop.yml
Rails/StrongParametersPermitAll:
  Enabled: true
  Include:
    - app/controllers/**/*.rb

How bad is the Active Storage direct-upload surface?

Bad enough to pay attention to. Active Storage's direct-upload flow signs a presigned URL using the Rails signing key; an attacker who obtains that key (via Heroku log exposure, GitHub leak, or stale CI artifact) can forge uploads that masquerade as attachments on any record. CVE-2024-26144 (March 2024, CVSS 6.5) allowed information leakage through Active Storage's URL signing when purpose: was misused. Mitigate by rotating secret_key_base on any suspected exposure, scoping direct-upload to tenant-specific S3 prefixes via IAM condition keys, and validating MIME type server-side after upload — never trust content_type from the client.

Is cookie rotation deployed?

Almost never, and it matters more in 2025. Rails 7.1 introduced config.action_dispatch.cookies_rotations for transparent key rotation; Rails 7.2 made per-cookie key IDs first-class. Enterprises that forked Devise, Clearance, or rolled their own sessions are typically stuck on a single secret_key_base forever. The impact: a single leaked secret compromises every session cookie ever signed. The fix, documented in the Rails Security Guide but rarely implemented:

Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
  cookies.rotate :encrypted, old_secret, cipher: "aes-256-gcm", digest: "SHA256"
end

Rotate quarterly on a schedule; rotate immediately on any credential exposure.

Are we still vulnerable to the bind_rails class of YAML deserialization?

If you use YAML.load anywhere, yes. YAML.unsafe_load remains in 11 of 14 audited codebases — typically in background jobs reading "trusted" config from S3 or Redis. CVE-2024-32465 in rubygems/rubygems (May 2024) was a reminder that trusted-source assumptions break when attackers can write to the trusted source. Replace unconditionally with YAML.safe_load_file(path, permitted_classes: [Time, Date, Symbol]) and add a lint rule:

# .semgrep.yml
- id: ruby-yaml-unsafe
  pattern-either:
    - pattern: YAML.unsafe_load(...)
    - pattern: YAML.load(...)
  message: "Use YAML.safe_load with explicit permitted_classes."
  severity: ERROR
  languages: [ruby]

What about Active Record SQL injection via order and find_by_sql?

Model.order(params[:sort]) remains the single most common AR injection we find — 9 of 14 codebases. Rails added #sanitize_sql_for_order but it is not default. Use a whitelist:

ALLOWED_SORTS = %w[created_at updated_at name email].freeze
scope :sorted, ->(col) { order(ALLOWED_SORTS.include?(col) ? col : :created_at) }

Similarly, find_by_sql([sanitized_string, *params]) is safe; find_by_sql("... #{params[:x]} ...") is not. The 2024 Gitlab CVE-2024-5655 was a variant of this pattern in the pipelines API.

Is the Rails 7.2 "prevent_writes" replica config tuned?

Only if you've had a prod outage from misrouted writes. Rails 7.2 tightened read/write splitting with preventing_writes?; combined with ActiveRecord::Base.connected_to(role: :reading), it stops accidental writes against replicas. More importantly for security, it surfaces queries that unexpectedly mutate data — a signal for controller paths that a threat modeler should review. Enable config.active_record.query_log_tags_enabled = true and annotate tags with controller, action, and tenant_id to correlate incidents against database audit logs.

How do we enforce CSP and Turbo frames correctly?

Rails 7.2 and 8.0 default to a reasonable CSP, but 7 of 14 audited apps had weakened it to unsafe-inline or unsafe-eval to accommodate legacy Stimulus controllers. The right fix is a nonce-based CSP:

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.default_src :self
  policy.script_src :self, :nonce
  policy.style_src :self, :nonce
end
Rails.application.config.content_security_policy_nonce_generator =
  ->(request) { SecureRandom.base64(16) }

Turbo 8 supports CSP nonces natively. If you are still running Turbo 7.x, upgrade — the nonce handling shipped March 2024.

How Safeguard Helps

Safeguard ingests Gemfile.lock and cargo/npm/python lockfiles to produce a unified CycloneDX SBOM that tracks Rails, Devise, and middleware version drift against RubySec and CVE feeds. Griffin AI performs reachability analysis to rank vulnerabilities by whether the vulnerable sink is actually invoked — a rails-html-sanitizer CVE matters far less if your app renders only trusted Markdown. TPRM workflows monitor upstream gem maintainers for the "abandoned gem" signal that drove CVE-2024-26141 in rack. Policy gates block merges that introduce gems without a matching license or with a CVSS above your threshold, and Safeguard's audit log produces SOC 2 CC7.1 evidence for every production Rails deploy.

Never miss an update

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