CVE-2023-45806 (published 2023-11-10T15:15Z)

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, if a user has been quoted and uses a `|` in their full name, they might be able to trigger a bug that generates a lot of duplicate content in all the posts they\'ve been quoted by updating their full name again. Version 3.1.3 of the `stable` branch and version 3.2.0.beta3 of the `beta` and `tests-passed` branches contain a patch for this issue. No known workaround exists, although one can stop the "bleeding" by ensuring users only use alphanumeric characters in their full name field.

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

Weakness information:
cwe_id: CWE-1333
cwe_name: Inefficient Regular Expression Complexity
description: The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles.
extended_description: Some regular expression engines have a feature called "backtracking". If the token cannot match, the engine "backtracks" to a position that may result in a different token that can match.
                Backtracking becomes a weakness if all of these conditions are met:
                
                    The number of possible backtracking attempts are exponential relative to the length of the input.
                    The input can fail to match the regular expression.
                    The input can be long enough.
                
		
		  Attackers can create crafted inputs that
		  intentionally cause the regular expression to use
		  excessive backtracking in a way that causes the CPU
		  consumption to spike.
		
            
            

Fix information:
repo_url: https://github.com/discourse/discourse
commit_details: repo_url: https://github.com/discourse/discourse
author: Régis Hanol
author_date: 2023-10-16 14:06:02+02:00
msg: SECURITY: escape display names

Ensure we escape the display names before passing it to the regexp used to update
quotes whenever a user change their display name.
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: quote_rewriter.rb
change_type: ModificationType.MODIFY
diff: @@ -6,17 +6,20 @@ class QuoteRewriter
   end
 
   def rewrite_raw_username(raw, old_username, new_username)
+    escaped_old_username = Regexp.escape(old_username)
     pattern =
       Regexp.union(
-        /(?<pre>\[quote\s*=\s*["'']?.*username:)#{old_username}(?<post>\,?[^\]]*\])/i,
-        /(?<pre>\[quote\s*=\s*["'']?)#{old_username}(?<post>\,?[^\]]*\])/i,
+        /(?<pre>\[quote\s*=\s*["'']?.*username:)#{escaped_old_username}(?<post>\,?[^\]]*\])/i,
+        /(?<pre>\[quote\s*=\s*["'']?)#{escaped_old_username}(?<post>\,?[^\]]*\])/i,
       )
 
     raw.gsub(pattern, "\\k<pre>#{new_username}\\k<post>")
   end
 
   def rewrite_cooked_username(cooked, old_username, new_username, avatar_img)
-    pattern = /(?<=\s)#{PrettyText::Helpers.format_username(old_username)}(?=:)/i
+    formatted_old_username = PrettyText::Helpers.format_username(old_username)
+    escaped_old_username = Regexp.escape(formatted_old_username)
+    pattern = /(?<=\s)#{escaped_old_username}(?=:)/i
 
     cooked
       .css("aside.quote")
@@ -42,13 +45,17 @@ class QuoteRewriter
   end
 
   def rewrite_raw_display_name(raw, old_display_name, new_display_name)
-    pattern = /(?<pre>\[quote\s*=\s*["'']?)#{old_display_name}(?<post>\,[^\]]*username[^\]]*\])/i
+    escaped_old_display_name = Regexp.escape(old_display_name)
+    pattern =
+      /(?<pre>\[quote\s*=\s*["'']?)#{escaped_old_display_name}(?<post>\,[^\]]*username[^\]]*\])/i
 
     raw.gsub(pattern, "\\k<pre>#{new_display_name}\\k<post>")
   end
 
   def rewrite_cooked_display_name(cooked, old_display_name, new_display_name)
-    pattern = /(?<=\s)#{PrettyText::Helpers.format_username(old_display_name)}(?=:)/i
+    formatted_old_display_name = PrettyText::Helpers.format_username(old_display_name)
+    escaped_old_display_name = Regexp.escape(formatted_old_display_name)
+    pattern = /(?<=\s)#{escaped_old_display_name}(?=:)/i
 
     cooked
       .css("aside.quote")

programming_language: Ruby
filename: change_display_name_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -3,7 +3,11 @@
 RSpec.describe Jobs::ChangeDisplayName do
   before { stub_image_size }
 
-  let(:user) { Fabricate(:user, username: "codinghorror", name: "Jeff") }
+  let(:username) { "codinghorror" }
+  let(:old_display_name) { "|| Jeff ||" }
+  let(:new_display_name) { "|| Mr. Atwood ||" }
+
+  let(:user) { Fabricate(:user, username: username, name: old_display_name) }
   let(:topic) { Fabricate(:topic, user: user) }
   let!(:post) { create_post(post_attributes.merge(topic_id: topic.id)) }
 
@@ -11,25 +15,25 @@ RSpec.describe Jobs::ChangeDisplayName do
   let(:avatar_url) { user.avatar_template_url.gsub("{size}", "48") }
 
   let(:post_attributes) { { raw: <<~RAW } }
-    [quote="Jeff, post:1, topic:#{quoted_post.topic.id}, username:codinghorror"]
+    [quote="#{old_display_name}, post:1, topic:#{quoted_post.topic.id}, username:#{username}"]
     quoted post
     [/quote]
   RAW
 
   let(:revised_post_attributes) { { raw: <<~RAW } }
-    [quote="Jeff, post:1, topic:#{quoted_post.topic.id}, username:codinghorror"]
+    [quote="#{old_display_name}, post:1, topic:#{quoted_post.topic.id}, username:#{username}"]
     quoted post
     [/quote]
     Forgot something.
   RAW
 
-  let(:args) { { user_id: user.id, old_name: "Jeff", new_name: "Mr. Atwood" } }
+  let(:args) { { user_id: user.id, old_name: old_display_name, new_name: new_display_name } }
 
   describe "#execute" do
     context "when the renamed user has been quoted" do
       it "rewrites the raw quote display name" do
         expect { described_class.new.execute(args) }.to change { post.reload.raw }.to(<<~RAW.strip)
-          [quote="Mr. Atwood, post:1, topic:#{quoted_post.topic.id}, username:codinghorror"]
+          [quote="#{new_display_name}, post:1, topic:#{quoted_post.topic.id}, username:#{username}"]
           quoted post
           [/quote]
         RAW
@@ -38,10 +42,10 @@ RSpec.describe Jobs::ChangeDisplayName do
       it "rewrites the cooked quote display name" do
         expect { described_class.new.execute(args) }.to change { post.reload.cooked }.to(
           match_html(<<~HTML.strip),
-          <aside class="quote no-group" data-username="codinghorror" data-post="1" data-topic="#{quoted_post.topic.id}">
+          <aside class="quote no-group" data-username="#{username}" data-post="1" data-topic="#{quoted_post.topic.id}">
           <div class="title">
           <div class="quote-controls"></div>
-          <img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar"> Mr. Atwood:</div>
+          <img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar"> #{new_display_name}:</div>
           <blockquote>
           <p>quoted post</p>
           </blockquote>
@@ -58,7 +62,7 @@ RSpec.describe Jobs::ChangeDisplayName do
         expect { described_class.new.execute(args) }.to change {
           post.reload.revisions[0].modifications["raw"][0]
         }.to(<<~RAW.strip)
-          [quote="Mr. Atwood, post:1, topic:#{quoted_post.topic.id}, username:codinghorror"]
+          [quote="#{new_display_name}, post:1, topic:#{quoted_post.topic.id}, username:#{username}"]
           quoted post
           [/quote]
           RAW

programming_language: Ruby