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.
Also available in Português (Brasil)
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:
- Removals of things that were deprecated in 8.0.
- Seven new framework defaults, which you can adopt one at a time.
- 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.msis gone from Active Support. It lives in thebenchmarkgem, which is a bundled gem on Ruby 4.0, so addgem "benchmark"if you call it.to_timenow always preserves the receiver's timezone. Deleteconfig.active_support.to_time_preserves_timezonefrom your config; the setting itself is deprecated.- Passing a
Timeobject toTime#since, and adding aTimeto anActiveSupport::TimeWithZone, are removed.
Active Job
config.active_job.enqueue_after_transaction_commitis removed, andActiveJob::Base.enqueue_after_transaction_commitno longer accepts:never,:alwaysor:default. Settrueorfalseon the job classes that need it.- The internal SuckerPunch adapter is removed; the
sucker_punchgem ships its own.
Action Pack
- Semicolons are no longer query string separators.
foo=bar;baz=quuxnow parses as a singlefooparam with the valuebar;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]=baris 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
:retriesoption is removed. - MySQL's
:unsigned_floatand:unsigned_decimalcolumn methods are removed. Old migrations that use them will fail if you ever replay migrations from scratch instead of loading the schema. - The
:azureActive Storage service is removed. If you still use it, move to a maintained adapter before the upgrade, not during it.
Railties
bin/rake statsandSTATS_DIRECTORIESare removed; usebin/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:
| Setting | New value | Risk | What to look for |
|---|---|---|---|
action_controller.action_on_path_relative_redirect | :raise | Medium | Redirects to strings without a leading slash |
active_record.raise_on_missing_required_finder_order_columns | true | Medium | first/last on models with no primary key |
action_view.render_tracker | :ruby | Low | Possible one-time fragment cache misses |
action_controller.escape_json_responses | false | Low | JSON responses inlined into HTML |
active_support.escape_js_separators_in_json | false | Low | Pre-2019 JavaScript consumers |
action_view.remove_hidden_field_autocomplete | true | Low | Exact-HTML test assertions |
config.yjit | !Rails.env.local? | Low | YJIT 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
sidekiqgem. Upgrade Sidekiq to a release that ships it;queue_adapter = :sidekiqstays the same. String#mb_charsandActiveSupport::Multibyte::Chars. Ruby's ownStringmethods have handled Unicode for years:name.mb_chars.upcasebecomesname.upcase.ActiveSupport::Configurable. Replace it withclass_attributeor plain accessors.ActiveRecord::Base.signed_id_verifier_secret. Move toRails.application.message_verifiers, orModel.signed_id_verifierif the secret is model-specific.- Bulk writes.
insert_all/upsert_allwith unpersisted records in associations, andupdate_allwithWITH,WITH RECURSIVEorDISTINCT. - Custom Active Job serializers must expose a public
#klassmethod. config.action_dispatch.ignore_leading_bracketsis 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)
- Move to the latest 8.0 patch, 8.0.5.1. You need it regardless: it is the CVE-2026-66066 fix.
- Green suite, zero deprecation warnings.
- Run the removal greps above and fix every hit.
- 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
- When
Gemfile.nextis green, make 8.1 the only version: point the Gemfile at~> 8.1.4, runbundle update rails, and delete the dual-boot scaffolding. - Run
bin/rails app:updateand review every conflict instead of accepting them blindly. Keepconfig.load_defaults 8.0for now. SetTHOR_MERGEif you want your own merge tool for the conflicts. - Update the
@rails/*npm packages inpackage.jsonto match, whether you use jsbundling or Vite. - Commit the alphabetized
schema.rbon its own.
Phase 4: deploy, then flip defaults in batches
- Deploy 1: Rails 8.1 with
load_defaults 8.0and the new defaults file untouched. Soak for 48 hours. Watch the logs for path-relative redirect warnings and finder-order deprecations. - Deploy 2: the low-risk batch: both JSON settings, hidden-field autocomplete, YJIT.
- Deploy 3:
render_tracker = :ruby, in a quiet window. - Deploy 4: finder order and path-relative redirects, once the warnings from deploy 1 are at zero.
- Delete
new_framework_defaults_8_1.rband setconfig.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, splitperformintosteps, and save a cursor withstep.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.rbandbin/cideclare your setup, lint, security and test steps in one Ruby DSL, with optionalgh signoffintegration. - Markdown rendering.
format.md { render markdown: @page }for any object withto_markdown. - Deprecated associations.
has_many :posts, deprecated: truereports every use, including preloads and nested attributes, in:warn,:raiseor:notifymode. 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,
:azurestorage migrated if used - No gems pinning Rails below 8.1
-
Gemfile.nextgreen in CI -
app:updatereviewed,load_defaults 8.0kept - Alphabetized
schema.rbin 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
- Ruby on Rails Maintenance Policy rubyonrails.org
- Ruby on Rails 8.1 Release Notes guides.rubyonrails.org
- Upgrading Ruby on Rails (Rails Guides) guides.rubyonrails.org
- Configuring Rails Applications: default values for 8.1 guides.rubyonrails.org
- new_framework_defaults_8_1.rb template (rails/rails, 8-1-stable) github.com
- rails/rails #53281: sort schema.rb columns alphabetically github.com
- All versions of rails (RubyGems.org) rubygems.org
- 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.