CVE-2021-41263 (published 2021-11-15T20:15Z)

rails_multisite provides multi-db support for Rails applications. In affected versions this vulnerability impacts any Rails applications using `rails_multisite` alongside Rails' signed/encrypted cookies. Depending on how the application makes use of these cookies, it may be possible for an attacker to re-use cookies on different 'sites' within a multi-site Rails application. The issue has been patched in v4 of the `rails_multisite` gem. Note that this upgrade will invalidate all previous signed/encrypted cookies. The impact of this invalidation will vary based on the application architecture.

Severity: MEDIUM
CVSS v2 base score: 6.0 (AV:N/AC:M/Au:S/C:P/I:P/A:P)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-327
cwe_name: Use of a Broken or Risky Cryptographic Algorithm
description: The product uses a broken or risky cryptographic algorithm or protocol.
extended_description: 
	   Cryptographic algorithms are the methods by which data is scrambled to prevent observation or influence by unauthorized actors. Insecure cryptography can be exploited to expose sensitive information, modify data in unexpected ways, spoof identities of other users or devices, or other impacts.
           It is very difficult to produce a secure algorithm, and even high-profile algorithms by accomplished cryptographic experts have been broken. Well-known techniques exist to break or weaken various kinds of cryptography. Accordingly, there are a small number of well-understood and heavily studied algorithms that should be used by most products. Using a non-standard or known-insecure algorithm is dangerous because a determined adversary may be able to break the algorithm and compromise whatever data has been protected.
           Since the state of cryptography advances so rapidly, it is common for an algorithm to be considered "unsafe" even if it was once thought to be strong. This can happen when new attacks are discovered, or if computing power increases so much that the cryptographic algorithm no longer provides the amount of protection that was originally thought.
	   For a number of reasons, this weakness is even more challenging to manage with hardware deployment of cryptographic algorithms as opposed to software implementation. First, if a flaw is discovered with hardware-implemented cryptography, the flaw cannot be fixed in most cases without a recall of the product, because hardware is not easily replaceable like software. Second, because the hardware product is expected to work for years, the adversary's computing power will only increase over time.
	 
         

Fix information:
repo_url: https://github.com/discourse/rails_multisite
commit_details: repo_url: https://github.com/discourse/rails_multisite
author: David Taylor
author_date: 2021-11-15 15:32:45+00:00
msg: SECURITY: Vary the encrypted/signed cookie salts per-hostname (#26)

This ensures Rails encrypted/signed cookies cannot be re-used across different sites which share the same secret_key_base
repository: repo_url: https://github.com/discourse/rails_multisite
repo_name: discourse/rails_multisite
description: Multi tenancy for Rails applications
repo_language: Ruby
owner: discourse
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,8 @@
+## 4.0.0 - 2021-11-15
+
+ * Vary the encrypted/signed cookie salts per-hostname (fix for CVE-2021-41263). This update will
+   cause existing cookies to be invalidated
+
 ## 3.1.0 - 2021-09-10
 
  * Make config file path configurable via `Rails.configuration.multisite_config_path`

programming_language: Markdown
filename: rails_multisite.rb
change_type: ModificationType.MODIFY
diff: @@ -7,3 +7,4 @@ require 'rails_multisite/railtie'
 require 'rails_multisite/formatter'
 require 'rails_multisite/connection_management'
 require 'rails_multisite/middleware'
+require 'rails_multisite/cookie_salt'

programming_language: Ruby
filename: cookie_salt.rb
change_type: ModificationType.ADD
diff: @@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module RailsMultisite
+  class CookieSalt
+    COOKIE_SALT_KEYS = [
+      "action_dispatch.signed_cookie_salt",
+      "action_dispatch.encrypted_cookie_salt",
+      "action_dispatch.encrypted_signed_cookie_salt",
+      "action_dispatch.authenticated_encrypted_cookie_salt"
+    ]
+
+    def self.update_cookie_salts(env:, host:)
+      COOKIE_SALT_KEYS.each { |key| env[key] = "#{env[key]} #{host}" }
+    end
+  end
+end

programming_language: Ruby
filename: middleware.rb
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,7 @@ module RailsMultisite
 
         ActiveRecord::Base.connection_handler.clear_active_connections!
         ConnectionManagement.establish_connection(host: host, db: db)
+        CookieSalt.update_cookie_salts(env: env, host: host)
         @app.call(env)
       ensure
         ActiveRecord::Base.connection_handler.clear_active_connections!

programming_language: Ruby
filename: version.rb
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 # frozen_string_literal: true
 #
 module RailsMultisite
-  VERSION = "3.1.0"
+  VERSION = "4.0.0"
 end

programming_language: Ruby
filename: middleware_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -2,6 +2,7 @@
 require 'spec_helper'
 require 'rails_multisite'
 require 'rack/test'
+require 'json'
 
 describe RailsMultisite::Middleware do
   include Rack::Test::Methods
@@ -22,6 +23,11 @@ describe RailsMultisite::Middleware do
           [200, { 'Content-Type' => 'text/html' }, "<html><BODY><h1>#{request.hostname}</h1></BODY>\n \t</html>"]
         end)
       end
+      map '/salts' do
+        run (proc do |env|
+          [200, { 'Content-Type' => 'application/json' }, env.slice(*RailsMultisite::CookieSalt::COOKIE_SALT_KEYS).to_json]
+        end)
+      end
     }.to_app
   end
 
@@ -98,4 +104,23 @@ describe RailsMultisite::Middleware do
       expect(last_response).to be_not_found
     end
   end
+
+  describe 'encrypted/signed cookie salts' do
+    it 'updates salts per-hostname' do
+      get 'http://default.localhost/salts'
+      expect(last_response).to be_ok
+      default_salts = JSON.parse(last_response.body)
+      expect(default_salts.keys).to contain_exactly(*RailsMultisite::CookieSalt::COOKIE_SALT_KEYS)
+      expect(default_salts.values).to all(include("default.localhost"))
+
+      get 'http://second.localhost/salts'
+      expect(last_response).to be_ok
+      second_salts = JSON.parse(last_response.body)
+      expect(second_salts.keys).to contain_exactly(*RailsMultisite::CookieSalt::COOKIE_SALT_KEYS)
+      expect(second_salts.values).to all(include("second.localhost"))
+
+      leaked_previous_hostname = second_salts.values.any? { |v| v.include?("default.localhost") }
+      expect(leaked_previous_hostname).to eq(false)
+    end
+  end
 end

programming_language: Ruby