CVE-2023-38685 (published 2023-07-28T16:15Z)

Discourse is an open source discussion platform. Prior to version 3.0.6 of the `stable` branch and version 3.1.0.beta7 of the `beta` and `tests-passed` branches, information about restricted-visibility topic tags could be obtained by unauthorized users. The issue is patched in version 3.0.6 of the `stable` branch and version 3.1.0.beta7 of the `beta` and `tests-passed` branches.

CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A: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
commit_details: repo_url: https://github.com/discourse/discourse
author: Bianca Nenciu
author_date: 2023-07-28 12:53:50+01:00
msg: SECURITY: Hide restricted tags in noscript view

The hidden tags are usually filtered out by the serializer, but the
noscript view uses the topic objects instead of the serialized objects.
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: topic.rb
change_type: ModificationType.MODIFY
diff: @@ -2045,6 +2045,10 @@ class Topic < ActiveRecord::Base
     private_message? && all_allowed_users.count > 2
   end
 
+  def visible_tags(guardian)
+    tags.reject { |tag| guardian.hidden_tag_names.include?(tag[:name]) }
+  end
+
   private
 
   def invite_to_private_message(invited_by, target_user, guardian)

programming_language: Ruby
filename: topic_tags_mixin.rb
change_type: ModificationType.MODIFY
diff: @@ -26,17 +26,20 @@ module TopicTagsMixin
 
   def all_tags
     return @tags if defined?(@tags)
+
+    tags = topic.visible_tags(scope)
+
     # Calling method `pluck` or `order` along with `includes` causing N+1 queries
     tags =
       (
         if SiteSetting.tags_sort_alphabetically
-          topic.tags.sort_by(&:name)
+          tags.sort_by(&:name)
         else
           topic_count_column = Tag.topic_count_column(scope)
-          topic.tags.sort_by { |tag| tag.public_send(topic_count_column) }.reverse
+          tags.sort_by { |tag| tag.public_send(topic_count_column) }.reverse
         end
       )
-    tags = tags.reject { |tag| scope.hidden_tag_names.include?(tag[:name]) } if !scope.is_staff?
+
     @tags = tags
   end
 end

programming_language: Ruby
filename: list.erb
change_type: ModificationType.MODIFY
diff: @@ -70,11 +70,11 @@
                   </span>
                 </a>
               <% end %>
-              <% if t.tags %>
+              <% if tags = t.visible_tags(guardian) %>
                 <div class="discourse-tags">
-                  <% t.tags.each_with_index do |tag, index| %>
+                  <% tags.each_with_index do |tag, index| %>
                     <a href='<%= tag.full_url %>' class='discourse-tag'><%= tag.name %></a>
-                    <% if index < t.tags.size - 1 %>,&nbsp;<% end %>
+                    <% if index < tags.size - 1 %>,&nbsp;<% end %>
                   <% end %>
                 </div>
               <% end %>

programming_language: CoffeeScript
filename: list_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -195,6 +195,28 @@ RSpec.describe ListController do
 
       expect(new_sql_queries_count).to be <= initial_sql_queries_count
     end
+
+    context "with topics with tags" do
+      let(:tag_group) { Fabricate.build(:tag_group) }
+      let(:tag_group_permission) { Fabricate.build(:tag_group_permission, tag_group: tag_group) }
+      let(:restricted_tag) { Fabricate(:tag) }
+      let(:public_tag) { Fabricate(:tag) }
+
+      before do
+        tag_group.tag_group_permissions << tag_group_permission
+        tag_group.save!
+        tag_group_permission.tag_group.tags << restricted_tag
+        topic.tags << [public_tag, restricted_tag]
+      end
+
+      it "does not show hidden tags" do
+        get "/latest"
+
+        expect(response.status).to eq(200)
+        expect(response.body).to include(public_tag.name)
+        expect(response.body).not_to include(restricted_tag.name)
+      end
+    end
   end
 
   describe "categories and X" do

programming_language: Ruby