{"id":15006,"date":"2026-10-26T07:00:00","date_gmt":"2026-10-26T12:00:00","guid":{"rendered":"https:\/\/www.allerin.com\/blog\/?p=15006"},"modified":"2026-10-24T10:22:40","modified_gmt":"2026-10-24T15:22:40","slug":"rails-readers-secrets-23-october-2026","status":"publish","type":"post","link":"https:\/\/www.allerin.com\/blog\/rails-readers-secrets-23-october-2026\/","title":{"rendered":"Rails tightens readers and secrets, 23 October 2026"},"content":{"rendered":"<div class=\"allerin-rails-reading\" style=\"overflow-wrap: break-word;\">\n<p>On 26 October 2026, Rails 8.0 is 12 calendar days from the security-support deadline currently published as <a href=\"https:\/\/rubyonrails.org\/maintenance\" target=\"_blank\" rel=\"noopener\">7 November<\/a>. The <a href=\"https:\/\/rubyonrails.org\/2026\/10\/23\/this-week-in-rails\" target=\"_blank\" rel=\"noopener\">23 October Rails weekly<\/a> gives an application team three checks worth carrying into its upgrade work: which old messages it accepts, who can read its local signing secret, and what a SQLite column rename preserves.<\/p>\n<p>The changes below are on <a href=\"https:\/\/github.com\/rails\/rails\/tree\/a0fd31e1a4e96c80d3184998c956607004fb7f8c\" target=\"_blank\" rel=\"noopener\">main<\/a>. They are absent from the checked released tags <a href=\"https:\/\/github.com\/rails\/rails\/tree\/v8.1.4\" target=\"_blank\" rel=\"noopener\">8.1.4<\/a> and <a href=\"https:\/\/github.com\/rails\/rails\/tree\/v8.0.5.1\" target=\"_blank\" rel=\"noopener\">8.0.5.1<\/a>. A merge is a reason to prepare a regression check, not a promise about a future release.<\/p>\n<h2 id=\"json-writers-can-still-have-marshal-readers\" style=\"scroll-margin-top: 125px;\">JSON writers can still have Marshal readers<\/h2>\n<p><a href=\"https:\/\/github.com\/rails\/rails\/pull\/59156\" target=\"_blank\" rel=\"noopener\">PR #59156<\/a> makes <code style=\"overflow-wrap: anywhere;\">config.load_defaults 8.2<\/code> select <code style=\"overflow-wrap: anywhere;\">:json<\/code> for Active Support messages. Applications retaining defaults from 7.1 through 8.1 continue to select <code style=\"overflow-wrap: anywhere;\">:json_allow_marshal<\/code>. An explicit serializer or rotation also needs its own inspection.<\/p>\n<p><a href=\"https:\/\/rubyonrails.org\/2023\/10\/5\/Rails-7-1-0-has-been-released\" target=\"_blank\" rel=\"noopener\">Rails 7.1, released in 2023<\/a>, adopted JSON message writing while retaining a Marshal reader. <a href=\"https:\/\/github.com\/rails\/rails\/pull\/48170\" target=\"_blank\" rel=\"noopener\">The original change<\/a> protected existing signed and encrypted messages during upgrades. That compatibility reader outlived the proposed transition. A JSON writer therefore does not prove that old Marshal messages have stopped arriving.<\/p>\n<p>This benign fixture makes the distinction visible. It ran against Active Support 8.1.4 and the pinned main source with Ruby 3.4.10, with the same output on both. The key and message exist only inside the example.<\/p>\n<pre style=\"max-width: 100%; overflow-x: auto; white-space: pre-wrap; overflow-wrap: anywhere;\"><code class=\"language-ruby\">require \"active_support\"\r\nrequire \"active_support\/message_verifier\"\r\n\r\nkey = \"synthetic-only-key\" * 4\r\nold = ActiveSupport::MessageVerifier.new(key, serializer: :marshal)\r\ntoken = old.generate({ \"example\" =&gt; 1 })\r\n[:json_allow_marshal, :json].each do |format|\r\n  reader = ActiveSupport::MessageVerifier.new(key, serializer: format)\r\n  begin\r\n    puts \"#{format}: accepts old message = #{!reader.verified(token).nil?}\"\r\n  rescue RuntimeError =&gt; error\r\n    raise unless error.message == \"Unsupported serialization format\"\r\n    puts \"#{format}: rejected (#{error.message})\"\r\n  end\r\nend\r\n<\/code><\/pre>\n<pre style=\"max-width: 100%; overflow-x: auto; white-space: pre-wrap; overflow-wrap: anywhere;\"><code class=\"language-text\">json_allow_marshal: accepts old message = true\r\njson: rejected (Unsupported serialization format)\r\n<\/code><\/pre>\n<p>For an application already on 7.1 or later, observe <code style=\"overflow-wrap: anywhere;\">message_serializer_fallback.active_support<\/code> before choosing <code style=\"overflow-wrap: anywhere;\">:json<\/code>. Account for long-lived tokens and rollback readers; a quiet observation period is evidence only for the traffic it covers. Applications still on 5.2 or 6.1 need the intervening upgrade and format transition first. The existing <a href=\"https:\/\/www.allerin.com\/blog\/rails-marshal-audit-checklist\/\">Marshal audit checklist<\/a> follows cookies, caches and columns separately. This week&#8217;s message default does not retire those other readers.<\/p>\n<h2 id=\"a-local-secret-needs-local-protection\" style=\"scroll-margin-top: 125px;\">A local secret needs local protection<\/h2>\n<p><a href=\"https:\/\/github.com\/rails\/rails\/pull\/59247\" target=\"_blank\" rel=\"noopener\">PR #59247<\/a> creates <code style=\"overflow-wrap: anywhere;\">tmp\/local_secret.txt<\/code> with mode <code style=\"overflow-wrap: anywhere;\">0600<\/code>. On non-Windows platforms it also attempts to tighten an existing file&#8217;s permissions while retaining its contents. Failure to change permissions is tolerated, so inspect the actual file rather than assuming startup repaired it.<\/p>\n<p><a href=\"https:\/\/github.com\/rails\/rails\/commit\/f75934f80dae8f275839f7cd5e2df3ce41876851\" target=\"_blank\" rel=\"noopener\">Rails 7.1 in 2023 renamed<\/a> <code style=\"overflow-wrap: anywhere;\">development_secret.txt<\/code> to <code style=\"overflow-wrap: anywhere;\">local_secret.txt<\/code> because development and test both used it. The <a href=\"https:\/\/github.com\/rails\/rails\/blob\/v7.1.0\/railties\/lib\/rails\/application.rb\" target=\"_blank\" rel=\"noopener\">tagged implementation<\/a> persists the generated key between runs. The new patch changes who can read that file without changing the key.<\/p>\n<p>In a temporary-directory check under umask <code style=\"overflow-wrap: anywhere;\">022<\/code>, the released 8.1.4 generation method produced <code style=\"overflow-wrap: anywhere;\">0644<\/code>; main produced <code style=\"overflow-wrap: anywhere;\">0600<\/code>. Main also tightened an existing <code style=\"overflow-wrap: anywhere;\">0644<\/code> fixture and preserved its contents. These are macOS file-mode observations, not Windows ACL tests. Review local workspaces and <code style=\"overflow-wrap: anywhere;\">SECRET_KEY_BASE_DUMMY<\/code> build paths. Production applications using an explicit secret are outside this file change, and tighter permissions cannot revoke a secret already disclosed.<\/p>\n<h2 id=\"a-sqlite-rename-should-preserve-the-schema-around-it\" style=\"scroll-margin-top: 125px;\">A SQLite rename should preserve the schema around it<\/h2>\n<p><a href=\"https:\/\/github.com\/rails\/rails\/pull\/59204\" target=\"_blank\" rel=\"noopener\">PR #59204<\/a> replaces table rebuilding with SQLite&#8217;s native column rename. <a href=\"https:\/\/rubyonrails.org\/2018\/4\/9\/Rails-5-2-0-final\" target=\"_blank\" rel=\"noopener\">Rails 5.2 in 2018<\/a> already <a href=\"https:\/\/github.com\/rails\/rails\/blob\/v5.2.0\/activerecord\/lib\/active_record\/connection_adapters\/sqlite3_adapter.rb\" target=\"_blank\" rel=\"noopener\">rebuilt tables for this operation<\/a>, accommodating SQLite&#8217;s earlier limited alteration support. <a href=\"https:\/\/www.sqlite.org\/releaselog\/3_25_0.html\" target=\"_blank\" rel=\"noopener\">SQLite 3.25.0 added column renaming in September 2018<\/a>.<\/p>\n<p>A disposable SQLite check exposed more than a migration exception. On Rails 8.1.4, renaming a column used by a partial index failed. A separate rename silently dropped a trigger, and renaming a string primary key lost its primary-key declaration. On pinned main, the index rename succeeded, the trigger survived and executed, and the renamed primary key remained. Both runs used SQLite 3.53.2 through sqlite3 2.9.6, so the Rails adapter choice caused the difference.<\/p>\n<p>For a SQLite application, rehearse the migration against a schema containing its dependent objects and check those objects afterward. A successful migration alone would have missed the dropped trigger. PostgreSQL and MySQL applications have no action for this SQLite-specific patch.<\/p>\n<h2 id=\"what-to-do-this-week\" style=\"scroll-margin-top: 125px;\">What to do this week<\/h2>\n<ul>\n<li>Record the application&#8217;s effective message serializer and fallback observations before changing readers.<\/li>\n<li>Inspect local-secret permissions and assess any earlier exposure separately from tightening the file.<\/li>\n<li>Add schema assertions around pending SQLite column renames, including triggers and primary keys.<\/li>\n<li>Recheck the <a href=\"https:\/\/www.allerin.com\/blog\/rails-support-calendar-explained\/\">support calendar<\/a> and released patch status before selecting the deployment target.<\/li>\n<\/ul>\n<p>Use the <a href=\"https:\/\/www.allerin.com\/services\/rails-upgrades\">Rails upgrade review<\/a> to connect these checks to the application&#8217;s next version step.<\/p>\n<h2 id=\"sources\" style=\"scroll-margin-top: 125px;\">Sources<\/h2>\n<ul>\n<li><a href=\"https:\/\/rubyonrails.org\/2026\/10\/23\/this-week-in-rails\" target=\"_blank\" rel=\"noopener\">Official weekly and its upstream links<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/rails\/rails\/pull\/59156\" target=\"_blank\" rel=\"noopener\">Message default and compatibility history<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/rails\/rails\/pull\/59247\" target=\"_blank\" rel=\"noopener\">Local-secret permissions and platform limits<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/rails\/rails\/pull\/59204\" target=\"_blank\" rel=\"noopener\">SQLite rename implementation and regression cases<\/a><\/li>\n<li><a href=\"https:\/\/guides.rubyonrails.org\/maintenance_policy.html#release-schedule\" target=\"_blank\" rel=\"noopener\">Rails maintenance policy and extension rule<\/a><\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>The October 23 Rails changes sharpen three upgrade checks: accepted message formats, local-secret permissions and SQLite schema preservation. Reproductions compare released Rails 8.1.4 with pinned main.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","_links_to":"","_links_to_target":""},"categories":[2037],"tags":[2089,2044,2088,2038,2087],"class_list":["post-15006","post","type-post","status-publish","format-standard","hentry","category-ruby-on-rails","tag-marshal","tag-rails-maintenance","tag-rails-serialization","tag-rails-upgrades","tag-sqlite"],"_links":{"self":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/15006","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/comments?post=15006"}],"version-history":[{"count":3,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/15006\/revisions"}],"predecessor-version":[{"id":15009,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/15006\/revisions\/15009"}],"wp:attachment":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/media?parent=15006"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/categories?post=15006"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/tags?post=15006"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}