Skip to content

Rails 8.0 loses security support on November 7: upgrading to 8.1 before the deadline

TL;DR

Rails 8.0 gets its last security fixes on November 7, 2026. Moving to 8.1 is a small upgrade: same Ruby 3.2 floor, a short list of removals, an alphabetized schema.rb and seven new defaults. Dual-boot both versions, deploy 8.1 with the old defaults, flip them in batches, and keep the 8.0 image for rollback.

Rails 8.0 stops getting security patches on November 7, 2026. Here is what changes between 8.0 and 8.1, the seven new defaults and which ones can bite, and a dual-boot plan you can finish before the deadline.

Published 13 min read

Also available in Português (Brasil)

Share

Rails 8.0 gets its last security fixes on November 7, 2026. If your app is still on 8.0 after that, the next Active Storage-class CVE ships for 8.1 and not for you. The good news: 8.0 to 8.1 is one of the smallest minor upgrades Rails has had. Here is what actually changes, which new defaults can bite, and a dual-boot plan you can finish in the three weeks left.

Why now

The Rails maintenance policy gives each minor series one year of bug fixes and two years of security fixes, counted from its first release. Rails 8.0.0 shipped on November 7, 2024. Bug fixes for 8.0 ended in May; security fixes end on November 7, 2026.

This year that date is not abstract. The most recent 8.0 release, 8.0.5.1 (July 29, 2026), was the patch for CVE-2026-66066, the Active Storage variant-processing file read and RCE that attackers turned into a live campaign at the end of August. The next bug of that class that lands after November 7 gets a fix for 8.1 only. "We will upgrade when we have to" turns into "we upgrade during an incident".

The target is Rails 8.1.4 (September 24, 2026), the current patch release. There is no 8.2 on RubyGems yet.

A note on the 8.1 dates: the maintenance page lists October 10, 2026 as the end of 8.1 bug fixes, a date that has already passed, while no newer series exists. The same policy extends support for the previous release when no new one ships within a year. Either way 8.1 is the supported line, and its security fixes run until October 10, 2027.

How big is this upgrade?

Small. Rails 8.0 and 8.1 share the same Ruby floor (3.2.0), so Ruby is not part of this job. If you want Ruby 4.0 as well, do it as a separate step; the Ruby 3.4 to 4.0 guide covers it. The official upgrade guide lists exactly one 8.0 to 8.1 item (schema.rb column order). The real work is three lists:

  1. Removals of things that were deprecated in 8.0.
  2. Seven new framework defaults, which you can adopt one at a time.
  3. New deprecations to clear now, because they become removals next.

For a typical Rails 8.0 app, budget a day of focused work for the switch plus a couple of days of soak per batch of defaults.

What breaks: the removals

If your suite on 8.0.5.1 runs without deprecation warnings, you are mostly covered already. Grep anyway, because deprecation warnings only fire for code your tests execute.

Active Support

  • Benchmark.ms is gone from Active Support. It lives in the benchmark gem, which is a bundled gem on Ruby 4.0, so add gem "benchmark" if you call it.
  • to_time now always preserves the receiver's timezone. Delete config.active_support.to_time_preserves_timezone from your config; the setting itself is deprecated.
  • Passing a Time object to Time#since, and adding a Time to an ActiveSupport::TimeWithZone, are removed.

Active Job

  • config.active_job.enqueue_after_transaction_commit is removed, and ActiveJob::Base.enqueue_after_transaction_commit no longer accepts :never, :always or :default. Set true or false on the job classes that need it.
  • The internal SuckerPunch adapter is removed; the sucker_punch gem ships its own.

Action Pack

  • Semicolons are no longer query string separators. foo=bar;baz=quux now parses as a single foo param with the value bar;baz=quux. Legacy clients and webhooks that build URLs by hand are where this shows up.
  • The parameter parser no longer skips a leading bracket: [foo]=bar is now the key "[foo]", not "foo".
  • A single route can no longer map to multiple paths.

Active Record and Active Storage

  • The SQLite3 adapter's :retries option is removed.
  • MySQL's :unsigned_float and :unsigned_decimal column methods are removed. Old migrations that use them will fail if you ever replay migrations from scratch instead of loading the schema.
  • The :azure Active Storage service is removed. If you still use it, move to a maintained adapter before the upgrade, not during it.

Railties

  • bin/rake stats and STATS_DIRECTORIES are removed; use bin/rails stats.

One pass finds most of these:

grep -rnE "Benchmark\.ms|to_time_preserves_timezone|enqueue_after_transaction_commit|unsigned_(float|decimal)|STATS_DIRECTORIES|rake stats" \
  app lib config db spec test
grep -n "Azure" config/storage.yml

The one diff everyone sees: schema.rb

Active Record now sorts table columns alphabetically in schema.rb, so dumps stay identical across machines instead of flip-flopping with migration order. The cost is paid once: the first dump after the upgrade rewrites most of the file.

Put that rewrite in its own commit with no real schema change, and tell the team before merging, or every open branch that touches the schema gets a conflict:

bin/rails db:schema:dump
git add db/schema.rb
git commit -m "Rails 8.1: alphabetize schema.rb columns (no schema change)"

If you depend on physical column order, structure.sql still preserves it.

The seven new defaults

bin/rails app:update generates config/initializers/new_framework_defaults_8_1.rb with each new default commented out. Until you uncomment a line or bump config.load_defaults, the app keeps 8.0 behaviour. Ordered by how likely they are to hurt:

SettingNew valueRiskWhat to look for
action_controller.action_on_path_relative_redirect:raiseMediumRedirects to strings without a leading slash
active_record.raise_on_missing_required_finder_order_columnstrueMediumfirst/last on models with no primary key
action_view.render_tracker:rubyLowPossible one-time fragment cache misses
action_controller.escape_json_responsesfalseLowJSON responses inlined into HTML
active_support.escape_js_separators_in_jsonfalseLowPre-2019 JavaScript consumers
action_view.remove_hidden_field_autocompletetrueLowExact-HTML test assertions
config.yjit!Rails.env.local?LowYJIT turns off in development and test

The last one is not in the generated initializer; it only arrives when you set config.load_defaults 8.1.

Path-relative redirects now raise

redirect_to "example.com" or redirect_to "@attacker.com" is treated as a path relative to the current URL, which is a classic building block for open redirects. With the new default, Rails raises ActionController::Redirecting::UnsafeRedirectError for relative URLs without a leading slash. redirect_to "/safe/path" keeps working.

The old default is :log, so the first 8.1 deploy already tells you where the problems are. If you would rather see them in your APM than grep logs, switch to :notify for a few days and subscribe:

# config/initializers/new_framework_defaults_8_1.rb
Rails.configuration.action_controller.action_on_path_relative_redirect = :notify

# config/initializers/unsafe_redirect_audit.rb (temporary)
ActiveSupport::Notifications.subscribe("unsafe_redirect.action_controller") do |event|
  Rails.logger.warn("[unsafe_redirect] #{event.payload.inspect}")
end

The usual suspects: redirect_to params[:return_to], stored "back to" URLs, and OAuth or SSO callbacks that round-trip a location. When the log stays empty for a few days, flip it to :raise.

Order-dependent finders without an order

Calling first, second, last and friends on a relation with no order is deprecated when the model has nothing to fall back on: no implicit_order_column, no query_constraints, no primary key. Most models have an id, so this only hits the odd ones: database views, join tables created with id: false, legacy tables. With the new default these raise. The generated file notes that the config option itself goes away in Rails 8.2, so raising becomes the only behaviour.

The fix is an explicit order, or a fallback column on the model:

class MonthlyRevenue < ApplicationRecord # backed by a SQL view, no primary key
  self.implicit_order_column = "month"
end

JSON escaping, twice

escape_json_responses = false stops the JSON renderer from escaping <, >, & and the U+2028/U+2029 separators in render json: responses. It is faster and harmless for anything that parses the response as JSON, which is every API client and every fetch. The only risk is code that takes a rendered JSON response body and inlines it into an HTML <script> tag. That is rare; grep for it before flipping.

escape_js_separators_in_json = false stops escaping U+2028 and U+2029 in Active Support's JSON encoding. Those characters were invalid inside JavaScript string literals until ECMAScript 2019, so this only matters if something still feeds your JSON to a pre-2019 engine through eval or JSONP. to_json keeps escaping <, > and & through Active Support's separate HTML-entities setting, so <%= raw @data.to_json %> inside a script tag stays as safe as it was.

A Ruby parser for template dependencies

render_tracker = :ruby makes Action View track dependencies between templates with a Ruby parser instead of regular expressions. It finds render calls the regex missed. Because cache digests are computed from those dependencies, some fragment cache keys can change when you flip it. Treat it like a cache-key change: do it on a quiet day, not before a traffic peak, and watch your cache hit rate.

Hidden inputs lose autocomplete="off"

Hidden inputs generated by form_tag, token_tag, method_tag and button_to stop rendering autocomplete="off". Browsers behave the same. Tests that compare exact HTML may not.

YJIT off in development and test

Under load_defaults 8.1, YJIT defaults to !Rails.env.local?: on in production, off in development and test. Production does not change. If you measured YJIT speeding up a long test suite, keep it on there:

# config/environments/test.rb
config.yjit = true

Deprecations to clear while you are here

These still work on 8.1, with warnings. Fix them in the same pass so the next upgrade is just as small:

  • Built-in Sidekiq adapter. The adapter is now provided by the sidekiq gem. Upgrade Sidekiq to a release that ships it; queue_adapter = :sidekiq stays the same.
  • String#mb_chars and ActiveSupport::Multibyte::Chars. Ruby's own String methods have handled Unicode for years: name.mb_chars.upcase becomes name.upcase.
  • ActiveSupport::Configurable. Replace it with class_attribute or plain accessors.
  • ActiveRecord::Base.signed_id_verifier_secret. Move to Rails.application.message_verifiers, or Model.signed_id_verifier if the secret is model-specific.
  • Bulk writes. insert_all/upsert_all with unpersisted records in associations, and update_all with WITH, WITH RECURSIVE or DISTINCT.
  • Custom Active Job serializers must expose a public #klass method.
  • config.action_dispatch.ignore_leading_brackets is deprecated along with the behaviour it controlled.

To make sure none slip through, have the test environment raise on deprecations while you work:

# config/environments/test.rb (during the upgrade)
config.active_support.deprecation = :raise

Step-by-step upgrade

Phase 1: get 8.0 clean (no Rails bump yet)

  1. Move to the latest 8.0 patch, 8.0.5.1. You need it regardless: it is the CVE-2026-66066 fix.
  2. Green suite, zero deprecation warnings.
  3. Run the removal greps above and fix every hit.
  4. Find gems that pin Rails below 8.1:
bundle exec ruby -e '
  Bundler.load.specs.each do |s|
    s.dependencies.each do |d|
      next unless d.name.match?(/\A(rails|railties|active|action)/)
      puts "#{s.name}: #{d}" if d.requirement.to_s.match?(/< ?8\.1|~> ?8\.0\./)
    end
  end'

Upgrade or replace each gem it prints before going further.

Phase 2: dual boot

Run both versions from the same branch so CI tells you exactly what 8.1 breaks, without blocking anyone still shipping on 8.0:

# Gemfile
def next?
  File.basename(__FILE__) == "Gemfile.next"
end

if next?
  gem "rails", "~> 8.1.4"
else
  gem "rails", "~> 8.0.5"
end
ln -s Gemfile Gemfile.next
cp Gemfile.lock Gemfile.next.lock
BUNDLE_GEMFILE=Gemfile.next bundle update rails
BUNDLE_GEMFILE=Gemfile.next bin/rails test   # or bundle exec rspec

The next_rails gem automates this setup if you prefer a tool. In GitHub Actions, a matrix over the Gemfile is enough; ruby/setup-ruby honours BUNDLE_GEMFILE for its bundle cache:

strategy:
  matrix:
    gemfile: [Gemfile, Gemfile.next]
env:
  BUNDLE_GEMFILE: ${{ matrix.gemfile }}

Phase 3: switch

  1. When Gemfile.next is green, make 8.1 the only version: point the Gemfile at ~> 8.1.4, run bundle update rails, and delete the dual-boot scaffolding.
  2. Run bin/rails app:update and review every conflict instead of accepting them blindly. Keep config.load_defaults 8.0 for now. Set THOR_MERGE if you want your own merge tool for the conflicts.
  3. Update the @rails/* npm packages in package.json to match, whether you use jsbundling or Vite.
  4. Commit the alphabetized schema.rb on its own.

Phase 4: deploy, then flip defaults in batches

  1. Deploy 1: Rails 8.1 with load_defaults 8.0 and the new defaults file untouched. Soak for 48 hours. Watch the logs for path-relative redirect warnings and finder-order deprecations.
  2. Deploy 2: the low-risk batch: both JSON settings, hidden-field autocomplete, YJIT.
  3. Deploy 3: render_tracker = :ruby, in a quiet window.
  4. Deploy 4: finder order and path-relative redirects, once the warnings from deploy 1 are at zero.
  5. Delete new_framework_defaults_8_1.rb and set config.load_defaults 8.1.

Phase 5: Kamal and rollback

With Kamal, each step is a tagged build and a kamal deploy. Keep the previous image so kamal rollback is one command (the Kamal 2 guide has the full setup). Watch error rate, p95 latency and job failure rate for 48 hours after each deploy.

Rollback is cheap because none of the 8.1 defaults touch the cache format, cookie serialization or message verifiers, so an 8.0 image can read everything an 8.1 image wrote. The schema.rb reorder is cosmetic. The one way to make rollback expensive is adopting 8.1-only APIs too early: an 8.0 worker cannot run a job class that includes ActiveJob::Continuable. Hold the new features until you are past the point where you would roll back.

What you get in return

Once you are settled on 8.1, these are worth a look:

  • Active Job continuations. Include ActiveJob::Continuable, split perform into steps, and save a cursor with step.advance!. An interrupted job resumes from the last completed step instead of starting over, which matters with Kamal's default of thirty seconds for job containers to shut down.
  • Structured events. Rails.event.notify("user.signup", user_id: 123) with tags, context and pluggable subscribers, for logs your pipeline can parse.
  • Local CI. config/ci.rb and bin/ci declare your setup, lint, security and test steps in one Ruby DSL, with optional gh signoff integration.
  • Markdown rendering. format.md { render markdown: @page } for any object with to_markdown.
  • Deprecated associations. has_many :posts, deprecated: true reports every use, including preloads and nested attributes, in :warn, :raise or :notify mode. It is the right tool for the next cleanup like this one.
  • Kamal secrets from credentials. KAMAL_REGISTRY_PASSWORD=$(rails credentials:fetch kamal.registry_password) in .kamal/secrets.
  • Verbose redirect logs in development: config.action_dispatch.verbose_redirect_logs = true.

Checklist

  • On 8.0.5.1, suite green, zero deprecation warnings
  • Removal greps clean, :azure storage migrated if used
  • No gems pinning Rails below 8.1
  • Gemfile.next green in CI
  • app:update reviewed, load_defaults 8.0 kept
  • Alphabetized schema.rb in its own commit
  • Deploy 1 soaked, redirect and finder warnings at zero
  • Defaults flipped in batches, then load_defaults 8.1
  • Previous image kept for kamal rollback
  • All of it before November 7

Summary

Rails 8.0 to 8.1 is a removals-and-defaults upgrade, not a rewrite: same Ruby floor, one noisy schema.rb diff, a dozen removed APIs and seven defaults you can adopt at your own pace. Two of those defaults deserve care (path-relative redirects and finders without an order), and both announce themselves in the logs before they break anything. Dual-boot, deploy 8.1 with the old defaults, flip in batches, and you are on a supported Rails well before the 8.0 security window closes.

Sources

  1. Ruby on Rails Maintenance Policy rubyonrails.org
  2. Ruby on Rails 8.1 Release Notes guides.rubyonrails.org
  3. Upgrading Ruby on Rails (Rails Guides) guides.rubyonrails.org
  4. Configuring Rails Applications: default values for 8.1 guides.rubyonrails.org
  5. new_framework_defaults_8_1.rb template (rails/rails, 8-1-stable) github.com
  6. rails/rails #53281: sort schema.rb columns alphabetically github.com
  7. All versions of rails (RubyGems.org) rubygems.org
  8. Rails security bulletin AV26-767 (Canadian Centre for Cyber Security) www.cyber.gc.ca

FAQ

When exactly does Rails 8.0 stop receiving security fixes?

On November 7, 2026, two years after Rails 8.0.0 was released on November 7, 2024. Bug fixes for 8.0 already ended in May 2026. After November 7, new vulnerabilities are fixed in 8.1 only.

Do I need to upgrade Ruby to move to Rails 8.1?

No. Rails 8.0 and 8.1 both require Ruby 3.2.0 or newer. If you also want Ruby 4.0, upgrade it as a separate step, before or after Rails, so you can tell which change caused which failure.

Is it safe to set config.load_defaults 8.1 right away?

It works for many apps, but it flips seven behaviours at once. The safer path is to deploy 8.1 with load_defaults 8.0, read the logs for path-relative redirect warnings and finder-order deprecations, then enable the new defaults in small batches.

Why did my whole schema.rb change after upgrading?

Rails 8.1 sorts table columns alphabetically in schema.rb so dumps are consistent across machines. The first dump rewrites the file once. Commit that rewrite on its own, with no real schema change, and use structure.sql if you need physical column order.

Can I roll back from 8.1 to 8.0 if something goes wrong?

Yes. None of the 8.1 defaults change the cache format, cookie serialization or message verifiers, so an 8.0 image can read what 8.1 wrote. Avoid 8.1-only APIs such as ActiveJob::Continuable until you are past the point where you would roll back.

Which new default is most likely to break my app?

The path-relative redirect check and the finder-order check. The first raises on redirect_to with a string that has no leading slash; the second raises on first or last for models without a primary key. Both log warnings under the old defaults, so you can find every case before flipping.

Comments

Sign in with GitHub to comment.

No comments yet.