From 66e8fe9cc63b86ce83b380a2c9563723affefffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Guitaut?= Date: Thu, 19 May 2022 16:58:31 +0200 Subject: [PATCH] DEV: Migrate existing cookies to Rails 7 format This patch introduces a cookies rotator as indicated in the Rails upgrade guide. This allows to migrate from the old SHA1 digest to the new SHA256 digest. --- config/application.rb | 3 ++ .../new_framework_defaults_7_0.rb | 2 +- lib/middleware/cookies_rotator.rb | 34 +++++++++++++++++++ spec/integration/multisite_cookies_spec.rb | 29 ++++++++++++++-- 4 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 lib/middleware/cookies_rotator.rb diff --git a/config/application.rb b/config/application.rb index 0c31d3a327c..d59e525da72 100644 --- a/config/application.rb +++ b/config/application.rb @@ -171,6 +171,9 @@ module Discourse require "middleware/discourse_public_exceptions" config.exceptions_app = Middleware::DiscoursePublicExceptions.new(Rails.public_path) + require "middleware/cookies_rotator" + config.middleware.insert_before ActionDispatch::Cookies, Middleware::CookiesRotator + require "discourse_js_processor" require "discourse_sourcemapping_url_processor" diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb index fdbf91e62fc..a52539798f3 100644 --- a/config/initializers/new_framework_defaults_7_0.rb +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -25,7 +25,7 @@ Rails.application.config.action_view.apply_stylesheet_media_default = false # # See upgrading guide for more information on how to build a rotator. # https://guides.rubyonrails.org/v7.0/upgrading_ruby_on_rails.html -# Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256 +Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256 # Change the digest class for ActiveSupport::Digest. # Changing this default means that for example Etags change and diff --git a/lib/middleware/cookies_rotator.rb b/lib/middleware/cookies_rotator.rb new file mode 100644 index 00000000000..03cc3272157 --- /dev/null +++ b/lib/middleware/cookies_rotator.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Implementing cookies rotator for Rails 7+ as a middleware because this will +# work in single site mode AND in multisite mode without leaking anything in +# `Rails.application.config.action_dispatch.cookies_rotations`. +module Middleware + class CookiesRotator + def initialize(app) + @app = app + end + + def call(env) + request = ActionDispatch::Request.new(env) + env[ + ActionDispatch::Cookies::COOKIES_ROTATIONS + ] = ActiveSupport::Messages::RotationConfiguration.new.tap do |cookies| + key_generator = + ActiveSupport::KeyGenerator.new( + request.secret_key_base, + iterations: 1000, + hash_digest_class: OpenSSL::Digest::SHA1, + ) + key_len = ActiveSupport::MessageEncryptor.key_len + + cookies.rotate( + :encrypted, + key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len), + ) + cookies.rotate(:signed, key_generator.generate_key(request.signed_cookie_salt)) + end + @app.call(env) + end + end +end diff --git a/spec/integration/multisite_cookies_spec.rb b/spec/integration/multisite_cookies_spec.rb index 9a24b4d0e2d..09d043d3d24 100644 --- a/spec/integration/multisite_cookies_spec.rb +++ b/spec/integration/multisite_cookies_spec.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true RSpec.describe "multisite", type: %i[multisite request] do + let!(:first_host) { get "http://test.localhost/session/csrf.json" } + it "works" do get "http://test.localhost/session/csrf.json" - expect(response.status).to eq(200) + expect(response).to have_http_status :ok cookie = CGI.escape(response.cookies["_forum_session"]) id1 = session["session_id"] @@ -11,7 +13,7 @@ RSpec.describe "multisite", type: %i[multisite request] do headers: { "Cookie" => "_forum_session=#{cookie};", } - expect(response.status).to eq(200) + expect(response).to have_http_status :ok id2 = session["session_id"] expect(id1).to eq(id2) @@ -20,10 +22,31 @@ RSpec.describe "multisite", type: %i[multisite request] do headers: { "Cookie" => "_forum_session=#{cookie};", } - expect(response.status).to eq(200) + expect(response).to have_http_status :ok id3 = session["session_id"] # Session cookie was rejected and rotated expect(id2).not_to eq(id3) end + + describe "Cookies rotator" do + let!(:rotations) { request.cookies_rotations } + let(:second_host) { get "http://test2.localhost/session/csrf.json" } + let(:global_rotations) { Rails.application.config.action_dispatch.cookies_rotations } + + it "adds different rotations for different hosts" do + first_host + expect(request.cookies_rotations).to have_attributes signed: rotations.signed, + encrypted: rotations.encrypted + + second_host + expect(request.cookies_rotations).not_to have_attributes signed: rotations.signed, + encrypted: rotations.encrypted + end + + it "doesn't change global rotations" do + second_host + expect(global_rotations).to have_attributes signed: [], encrypted: [] + end + end end