CVE-2023-47121: Server-Side Request Forgery (SSRF) (CWE-918)
value: Discourse is an open source platform for community discussion. Prior to version 3.1.3 of the `stable` branch and version 3.2.0.beta3 of the `beta` and `tests-passed` branches, the embedding feature is susceptible to server side request forgery. 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. As a workaround, disable the Embedding feature.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 9.8
Published: 2023-11-10T16:15Z
Repository: https://github.com/discourse/discourse
Language: Ruby
Files changed: app/models/topic_embed.rb, spec/models/topic_embed_spec.rb
Commit date: 2023-11-09 13:39:08 +1100
Commit message: SECURITY: SSRF vulnerability in TopicEmbed

Block redirects when making the final request in TopicEmbed to prevent Server Side Request Forgery (SSRF)

Diff: diff --git a/app/models/topic_embed.rb b/app/models/topic_embed.rb
index 3ac5de7d8f83..15819bd26043 100644
--- a/app/models/topic_embed.rb
+++ b/app/models/topic_embed.rb
@@ -124,12 +124,12 @@ class TopicEmbed < ActiveRecord::Base
 
     uri = fd.resolve
     return if uri.blank?
 
     begin
-      html = uri.read
-    rescue OpenURI::HTTPError, Net::OpenTimeout
+      html = FinalDestination::HTTP.get(uri)
+    rescue OpenURI::HTTPError, Net::OpenTimeout, FinalDestination::SSRFDetector::DisallowedIpError
       return
     end
 
     parse_html(html, uri.to_s)
   end
diff --git a/spec/models/topic_embed_spec.rb b/spec/models/topic_embed_spec.rb
index 91aa671cb7b4..4b0aab7ab548 100644
--- a/spec/models/topic_embed_spec.rb
+++ b/spec/models/topic_embed_spec.rb
@@ -297,10 +297,23 @@ RSpec.describe TopicEmbed do
       it "scrubs the title when the option is enabled" do
         SiteSetting.embed_title_scrubber = " - Classic Books$"
         response = TopicEmbed.find_remote(url)
         expect(response.title).to eq("Through the Looking Glass")
       end
+
+      it "doesn't follow redirect when making request" do
+        FinalDestination.any_instance.stubs(:resolve).returns(URI("https://redirect.com"))
+        stub_request(:get, "https://redirect.com/").to_return(
+          status: 301,
+          body: "<title>Moved permanently</title>",
+          headers: {
+            "Location" => "https://www.example.org/",
+          },
+        )
+        response = TopicEmbed.find_remote(url)
+        expect(response.title).to eq("Moved permanently")
+      end
     end
 
     context 'with post with allowed classes "foo" and "emoji"' do
       fab!(:user) { Fabricate(:user) }
       let(:url) { "http://eviltrout.com/123" }

Vulnerable code:       html = uri.read
    rescue OpenURI::HTTPError, Net::OpenTimeout
Fixed code:       html = FinalDestination::HTTP.get(uri)
    rescue OpenURI::HTTPError, Net::OpenTimeout, FinalDestination::SSRFDetector::DisallowedIpError

      it "doesn't follow redirect when making request" do
        FinalDestination.any_instance.stubs(:resolve).returns(URI("https://redirect.com"))
        stub_request(:get, "https://redirect.com/").to_return(
          status: 301,
          body: "<title>Moved permanently</title>",
          headers: {
            "Location" => "https://www.example.org/",
          },
        )
        response = TopicEmbed.find_remote(url)
        expect(response.title).to eq("Moved permanently")
      end