CVE-2023-23620: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: Discourse is an open-source discussion platform. Prior to version 3.0.1 on the `stable` branch and 3.1.0.beta2 on the `beta` and `tests-passed` branches, the contents of latest/top routes for restricted tags can be accessed by unauthorized users. This issue is patched in version 3.0.1 on the `stable` branch and 3.1.0.beta2 on the `beta` and `tests-passed` branches. There are no known workarounds.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 5.3
Published: 2023-01-28T00:15Z
Repository: https://github.com/discourse/discourse
Language: Ruby
Files changed: app/controllers/tags_controller.rb, spec/requests/tags_controller_spec.rb
Commit date: 2023-01-25 18:55:55 +0200
Commit message: SECURITY: only show restricted tag lists to authorized users (#20004)

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
Diff: diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 1108b618cc74..aa5037e2ea1b 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -3,16 +3,17 @@
 class TagsController < ::ApplicationController
   include TopicListResponder
   include TopicQueryParams
 
   before_action :ensure_tags_enabled
-  before_action :ensure_visible, only: %i[show info]
 
   def self.show_methods
     Discourse.anonymous_filters.map { |f| :"show_#{f}" }
   end
 
+  before_action :ensure_visible, only: [:show, :info, *show_methods]
+
   requires_login except: [:index, :show, :tag_feed, :search, :info, *show_methods]
 
   skip_before_action :check_xhr, only: [:tag_feed, :show, :index, *show_methods]
 
   before_action :set_category,
diff --git a/spec/requests/tags_controller_spec.rb b/spec/requests/tags_controller_spec.rb
index 834589618be1..ac85694fc7cd 100644
--- a/spec/requests/tags_controller_spec.rb
+++ b/spec/requests/tags_controller_spec.rb
@@ -674,10 +674,22 @@ RSpec.describe TagsController do
           get "/tag/#{tag.name}/l/bookmarks.json"
 
           expect(response.status).to eq(200)
         end
 
+        it "returns a 404 when tag is restricted" do
+          tag_group = Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["test"])
+
+          get "/tag/test/l/latest.json"
+          expect(response.status).to eq(404)
+
+          sign_in(admin)
+
+          get "/tag/test/l/latest.json"
+          expect(response.status).to eq(200)
+        end
+
         context "with muted tags" do
           before do
             TagUser.create!(
               user_id: user.id,
               tag_id: tag.id,
@@ -753,10 +765,22 @@ RSpec.describe TagsController do
 
     it "raises an error if the period is not valid" do
       get "/tag/#{tag.name}/l/top.json?period=decadely"
       expect(response.status).to eq(400)
     end
+
+    it "returns a 404 if tag is restricted" do
+      tag_group = Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["test"])
+
+      get "/tag/test/l/top.json"
+      expect(response.status).to eq(404)
+
+      sign_in(admin)
+
+      get "/tag/test/l/top.json"
+      expect(response.status).to eq(200)
+    end
   end
 
   describe "#search" do
     context "with tagging disabled" do
       it "returns 404" do

Vulnerable code:   before_action :ensure_visible, only: %i[show info]
Fixed code:   before_action :ensure_visible, only: [:show, :info, *show_methods]

        it "returns a 404 when tag is restricted" do
          tag_group = Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["test"])

          get "/tag/test/l/latest.json"
          expect(response.status).to eq(404)

          sign_in(admin)

          get "/tag/test/l/latest.json"
          expect(response.status).to eq(200)
        end


    it "returns a 404 if tag is restricted" do
      tag_group = Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["test"])

      get "/tag/test/l/top.json"
      expect(response.status).to eq(404)

      sign_in(admin)

      get "/tag/test/l/top.json"
      expect(response.status).to eq(200)
    end