CVE-2023-47120 (published 2023-11-10T16:15Z)

Discourse is an open source platform for community discussion. In versions 3.1.0 through 3.1.2 of the `stable` branch and versions 3.1.0,beta6 through 3.2.0.beta2 of the `beta` and `tests-passed` branches, Redis memory can be depleted by crafting a site with an abnormally long favicon URL and drafting multiple posts which Onebox it. The issue is patched in version 3.1.3 of the `stable` branch and version 3.2.0.beta3 of the `beta` and `tests-passed` branches. There are no known workarounds.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-770
cwe_name: Allocation of Resources Without Limits or Throttling
description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
extended_description: Code frequently has to work with limited resources, so programmers must be careful to ensure that resources are not consumed too quickly, or too easily.  Without use of quotas, resource limits, or other protection mechanisms, it can be easy for an attacker to consume many resources by rapidly making many requests, or causing larger resources to be used than is needed. When too many resources are allocated, or if a single resource is too large, then it can prevent the code from working correctly, possibly leading to a denial of service.
         

Fix information:
repo_url: https://github.com/discourse/discourse
commit_details: repo_url: https://github.com/discourse/discourse
author: Ted Johansson
author_date: 2023-10-18 10:29:36+08:00
msg: SECURITY: Prevent Onebox cache overflow by limiting downloads and URL lengths
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: onebox.rb
change_type: ModificationType.MODIFY
diff: @@ -16,7 +16,7 @@ module Onebox
   DEFAULTS = {
     connect_timeout: 5,
     timeout: 10,
-    max_download_kb: (10 * 1024), # 10MB
+    max_download_kb: 2048, # 2MB
     load_paths: [File.join(Rails.root, "lib/onebox/templates")],
     allowed_ports: [80, 443],
     allowed_schemes: %w[http https],

programming_language: Ruby
filename: standard_embed.rb
change_type: ModificationType.MODIFY
diff: @@ -98,7 +98,13 @@ module Onebox
           ).first
         favicon = favicon.nil? ? nil : (favicon["href"].nil? ? nil : favicon["href"].strip)
 
-        Onebox::Helpers.get_absolute_image_url(favicon, url)
+        return nil if favicon.blank?
+
+        absolute_url = Onebox::Helpers.get_absolute_image_url(favicon, url)
+
+        return nil if absolute_url.length > UrlHelper::MAX_URL_LENGTH
+
+        absolute_url
       end
 
       def get_description

programming_language: Ruby
filename: url_helper.rb
change_type: ModificationType.MODIFY
diff: @@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class UrlHelper
-  MAX_URL_LENGTH = 100_000
+  MAX_URL_LENGTH = 2_000
 
   # At the moment this handles invalid URLs that browser address bar accepts
   # where second # is not encoded

programming_language: Ruby
filename: standard_embed_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -66,6 +66,34 @@ RSpec.describe Onebox::Engine::StandardEmbed do
       expect(instance.raw).to eq({ title: "do not override me" })
     end
 
+    it "sets favicon URL" do
+      html_doc =
+        mocked_html_doc(
+          twitter_data: {
+            "name" => "twitter:url",
+            "content" => "cool.url",
+          },
+          favicon_url: "https://favicon.co/default.ico",
+        )
+      Onebox::Helpers.stubs(fetch_html_doc: html_doc)
+
+      expect(instance.raw).to eq({ url: "cool.url", favicon: "https://favicon.co/default.ico" })
+    end
+
+    it "ignores suspiciously long favicon URLs" do
+      html_doc =
+        mocked_html_doc(
+          twitter_data: {
+            "name" => "twitter:url",
+            "content" => "cool.url",
+          },
+          favicon_url: "https://favicon.co/#{"a" * 2_000}.ico",
+        )
+      Onebox::Helpers.stubs(fetch_html_doc: html_doc)
+
+      expect(instance.raw).to eq({ url: "cool.url" })
+    end
+
     it "sets oembed data" do
       Onebox::Helpers.stubs(fetch_html_doc: nil)
       Onebox::Oembed.any_instance.stubs(:data).returns({ description: "description" })
@@ -84,11 +112,11 @@ RSpec.describe Onebox::Engine::StandardEmbed do
 
   private
 
-  def mocked_html_doc(twitter_data: nil)
+  def mocked_html_doc(twitter_data: nil, favicon_url: nil)
     html_doc = mock
     html_doc.stubs(at_css: nil, at: nil)
     stub_twitter(html_doc, twitter_data)
-    stub_favicon(html_doc)
+    stub_favicon(html_doc, favicon_url)
     stub_json_ld
     html_doc
   end
@@ -97,13 +125,13 @@ RSpec.describe Onebox::Engine::StandardEmbed do
     html_doc.expects(:css).with("meta").at_least_once.returns([twitter_data])
   end
 
-  def stub_favicon(html_doc)
+  def stub_favicon(html_doc, favicon_url = nil)
     html_doc
       .stubs(:css)
       .with(
         'link[rel="shortcut icon"], link[rel="icon shortcut"], link[rel="shortcut"], link[rel="icon"]',
       )
-      .returns([])
+      .returns([{ "href" => favicon_url }.compact])
   end
 
   def stub_json_ld

programming_language: Ruby
filename: url_helper_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -166,7 +166,7 @@ RSpec.describe UrlHelper do
     end
 
     it "raises error if too long" do
-      expect do UrlHelper.normalized_encode("https://#{"a" * 100_000}.com") end.to raise_error(
+      expect do UrlHelper.normalized_encode("https://#{"a" * 2_000}.com") end.to raise_error(
         ArgumentError,
       )
     end

programming_language: Ruby