CVE-2024-31219 (published 2024-04-15T18:15Z)

Discourse-reactions is a plugin that allows user to add their reactions to the post. When whispers are enabled on a site via `whispers_allowed_groups` and reactions are made on whispers on public topics, the contents of the whisper and the reaction data are shown on the `/u/:username/activity/reactions` endpoint.\n


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/discourse/discourse-reactions
commit_details: repo_url: https://github.com/discourse/discourse-reactions
author: Martin Brennan
author_date: 2024-04-15 13:40:26+10:00
msg: SECURITY: Hide whisper posts in reactions given endpoint (#286)

Users who were not allowed to whisper could see reactions
and whisper content of posts on public topics.
repository: repo_url: https://github.com/discourse/discourse-reactions
repo_name: discourse/discourse-reactions
repo_language: Ruby
owner: discourse
file_changes: filename: custom_reactions_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -48,8 +48,12 @@ class DiscourseReactions::CustomReactionsController < ApplicationController
 
     reaction_users =
       DiscourseReactions::ReactionUser
-        .joins(:reaction, :post)
-        .joins("INNER JOIN topics t ON t.id = posts.topic_id AND t.deleted_at IS NULL")
+        .joins(
+          "INNER JOIN discourse_reactions_reactions ON discourse_reactions_reactions.id = discourse_reactions_reaction_users.reaction_id",
+        )
+        .joins("INNER JOIN posts p ON p.id = discourse_reactions_reaction_users.post_id")
+        .joins("INNER JOIN topics t ON t.id = p.topic_id AND t.deleted_at IS NULL")
+        .joins("INNER JOIN posts p2 ON p2.topic_id = t.id AND p2.post_number = 1")
         .joins("LEFT JOIN categories c ON c.id = t.category_id")
         .includes(:user, :post, :reaction)
         .where(user_id: user.id)
@@ -271,8 +275,7 @@ class DiscourseReactions::CustomReactionsController < ApplicationController
 
   def secure_reaction_users!(reaction_users)
     builder = DB.build("/*where*/")
-    UserAction.filter_private_messages(builder, current_user.id, guardian)
-    UserAction.filter_categories(builder, guardian)
+    UserAction.apply_common_filters(builder, current_user.id, guardian)
     reaction_users.where(builder.to_sql.delete_prefix("/*where*/").delete_prefix("WHERE"))
   end
 

programming_language: Ruby
filename: plugin.rb
change_type: ModificationType.MODIFY
diff: @@ -3,7 +3,7 @@
 # name: discourse-reactions
 # about: Allows users to react to a post with emojis.
 # meta_topic_id: 183261
-# version: 0.3
+# version: 0.5
 # author: Ahmed Gagan, Rafael dos Santos Silva, Kris Aubuchon, Joffrey Jaffeux, Kris Kotlarek, Jordan Vidrine
 # url: https://github.com/discourse/discourse-reactions
 

programming_language: Ruby
filename: custom_reactions_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -13,10 +13,16 @@ describe DiscourseReactions::CustomReactionsController do
   fab!(:post_2) { Fabricate(:post, user: user_1) }
   fab!(:private_topic) { Fabricate(:private_message_topic, user: user_2, recipient: admin) }
   fab!(:private_post) { Fabricate(:post, topic: private_topic) }
+  fab!(:whisper_post) do
+    Fabricate(:post, topic: Fabricate(:topic), post_type: Post.types[:whisper])
+  end
   fab!(:laughing_reaction) { Fabricate(:reaction, post: post_2, reaction_value: "laughing") }
   fab!(:open_mouth_reaction) { Fabricate(:reaction, post: post_2, reaction_value: "open_mouth") }
   fab!(:hugs_reaction) { Fabricate(:reaction, post: post_2, reaction_value: "hugs") }
   fab!(:hugs_reaction_private) { Fabricate(:reaction, post: private_post, reaction_value: "hugs") }
+  fab!(:laughing_reaction_whisper) do
+    Fabricate(:reaction, post: whisper_post, reaction_value: "laughing")
+  end
   fab!(:like) do
     Fabricate(
       :post_action,
@@ -40,6 +46,9 @@ describe DiscourseReactions::CustomReactionsController do
   fab!(:reaction_user_5) do
     Fabricate(:reaction_user, reaction: hugs_reaction_private, user: admin, post: private_post)
   end
+  fab!(:reaction_user_6) do
+    Fabricate(:reaction_user, reaction: laughing_reaction_whisper, user: user_2, post: whisper_post)
+  end
 
   before do
     SiteSetting.discourse_reactions_enabled = true
@@ -225,6 +234,27 @@ describe DiscourseReactions::CustomReactionsController do
       expect(response.parsed_body.map { |reaction| reaction["post_id"] }).to include(secure_post.id)
     end
 
+    it "does not return reactions for whispers if the user is not in whispers_allowed_groups" do
+      sign_in(user_1)
+
+      get "/discourse-reactions/posts/reactions.json", params: { username: user_2.username }
+
+      parsed = response.parsed_body
+      expect(response.parsed_body.map { |reaction| reaction["post_id"] }).not_to include(
+        whisper_post.id,
+      )
+
+      SiteSetting.whispers_allowed_groups = Group::AUTO_GROUPS[:trust_level_0].to_s
+      Group.refresh_automatic_groups!
+
+      get "/discourse-reactions/posts/reactions.json", params: { username: user_2.username }
+
+      parsed = response.parsed_body
+      expect(response.parsed_body.map { |reaction| reaction["post_id"] }).to include(
+        whisper_post.id,
+      )
+    end
+
     describe "a post with one of your reactions has been deleted" do
       fab!(:deleted_post) { Fabricate(:post) }
       fab!(:kept_post) { Fabricate(:post) }

programming_language: Ruby