CVE-2022-23641 (published 2022-02-15T21:15Z)

Discourse is an open source discussion platform. In versions prior to 2.8.1 in the `stable` branch, 2.9.0.beta2 in the `beta` branch, and 2.9.0.beta2 in the `tests-passed` branch, users can trigger a Denial of Service attack by posting a streaming URL. Parsing Oneboxes in the background job trigger an infinite loop, which cause memory leaks. This issue is patched in version 2.8.1 of the `stable` branch, 2.9.0.beta2 of the `beta` branch, and 2.9.0.beta2 of the `tests-passed` branch. As a workaround, disable onebox in admin panel completely or specify allow list of domains that will be oneboxed.

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

Weakness information:
cwe_id: CWE-835
cwe_name: Loop with Unreachable Exit Condition ('Infinite Loop')
description: The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
extended_description: If the loop can be influenced by an attacker, this weakness could allow attackers to consume excessive resources such as CPU or memory.
         

Fix information:
repo_url: https://github.com/discourse/discourse
commit_details: repo_url: https://github.com/discourse/discourse
author: Krzysztof Kotlarek
author_date: 2022-02-14 12:11:09+11:00
msg: SECURITY: Onebox response timeout and size limit (#15927)

Validation to ensure that Onebox request is no longer than 10 seconds and response size is not bigger than 1 MB
repository: repo_url: https://github.com/discourse/discourse
repo_name: discourse/discourse
description: A platform for community discussion. Free, open, simple.
repo_language: Ruby
owner: discourse
file_changes: filename: final_destination.rb
change_type: ModificationType.MODIFY
diff: @@ -8,6 +8,8 @@ require 'url_helper'
 
 # Determine the final endpoint for a Web URI, following redirects
 class FinalDestination
+  MAX_REQUEST_TIME_SECONDS = 10
+  MAX_REQUEST_SIZE_BYTES = 1_048_576 # 1024 * 1024
 
   def self.clear_https_cache!(domain)
     key = redis_https_key(domain)
@@ -203,12 +205,21 @@ class FinalDestination
     middlewares = Excon.defaults[:middlewares]
     middlewares << Excon::Middleware::Decompress if @http_verb == :get
 
+    request_start_time = Time.now
+    response_body = +""
+    request_validator = lambda do |chunk, _remaining_bytes, _total_bytes|
+      response_body << chunk
+      raise Excon::Errors::ExpectationFailed.new("response size too big: #{@uri.to_s}") if response_body.bytesize > MAX_REQUEST_SIZE_BYTES
+      raise Excon::Errors::ExpectationFailed.new("connect timeout reached: #{@uri.to_s}") if Time.now - request_start_time > MAX_REQUEST_TIME_SECONDS
+    end
+
     response = Excon.public_send(@http_verb,
       @uri.to_s,
       read_timeout: timeout,
       connect_timeout: timeout,
       headers: headers,
-      middlewares: middlewares
+      middlewares: middlewares,
+      response_block: request_validator
     )
 
     location = nil
@@ -220,12 +231,12 @@ class FinalDestination
       # Cache body of successful `get` requests
       if @http_verb == :get
         if Oneboxer.cache_response_body?(@uri)
-          Oneboxer.cache_response_body(@uri.to_s, response.body)
+          Oneboxer.cache_response_body(@uri.to_s, response_body)
         end
       end
 
       if @follow_canonical
-        next_url = fetch_canonical_url(response.body)
+        next_url = fetch_canonical_url(response_body)
 
         if next_url.to_s.present? && next_url != @uri
           @follow_canonical = false

programming_language: Ruby
filename: final_destination_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -49,6 +49,13 @@ describe FinalDestination do
     }
   end
 
+  let(:body_response) do
+    {
+      status: 200,
+      body: "<body>test</body>"
+    }
+  end
+
   def canonical_follow(from, dest)
     stub_request(:get, from).to_return(
       status: 200,
@@ -182,6 +189,20 @@ describe FinalDestination do
       end
     end
 
+    it 'raises error when response is too big' do
+      stub_const(described_class, "MAX_REQUEST_SIZE_BYTES", 1) do
+        stub_request(:get, "https://codinghorror.com/blog").to_return(body_response)
+        final = FinalDestination.new('https://codinghorror.com/blog', opts.merge(follow_canonical: true))
+        expect { final.resolve }.to raise_error(Excon::Errors::ExpectationFailed, "response size too big: https://codinghorror.com/blog")
+      end
+    end
+
+    it 'raises error when response is too slow' do
+      stub_request(:get, "https://codinghorror.com/blog").to_return(lambda { |request| freeze_time(11.seconds.from_now) ; body_response })
+      final = FinalDestination.new('https://codinghorror.com/blog', opts.merge(follow_canonical: true))
+      expect { final.resolve }.to raise_error(Excon::Errors::ExpectationFailed, "connect timeout reached: https://codinghorror.com/blog")
+    end
+
     context 'follows canonical links' do
       it 'resolves the canonical link as the final destination' do
         canonical_follow("https://eviltrout.com", "https://codinghorror.com/blog")

programming_language: Ruby