CVE-2023-22453: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: Discourse is an option source discussion platform. Prior to version 2.8.14 on the `stable` branch and version 3.0.0.beta16 on the `beta` and `tests-passed` branches, the number of times a user posted in an arbitrary topic is exposed to unauthorized users through the `/u/username.json` endpoint. The issue is patched in version 2.8.14 and 3.0.0.beta16. There is no known workaround.
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-05T20:15Z
Repository: https://github.com/discourse/discourse
Language: Ruby
Files changed: app/controllers/users_controller.rb, spec/requests/users_controller_spec.rb
Commit date: 2023-01-05 06:08:19 +0800
Commit message: SECURITY: Don't expose user post counts to users who can't see the topic (#19728)

Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>

Co-authored-by: Daniel Waterworth <me@danielwaterworth.com>
Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
Diff: diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 1cc1d0e2b4f9..f7debba7844f 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -70,11 +70,11 @@ class UsersController < ApplicationController
     if guardian.can_see_profile?(@user)
       serializer_class = for_card ? UserCardSerializer : UserSerializer
       user_serializer = serializer_class.new(@user, scope: guardian, root: 'user')
 
       topic_id = params[:include_post_count_for].to_i
-      if topic_id != 0
+      if topic_id != 0 && guardian.can_see?(Topic.find_by_id(topic_id))
         user_serializer.topic_post_count = { topic_id => Post.secured(guardian).where(topic_id: topic_id, user_id: @user.id).count }
       end
     else
       user_serializer = HiddenProfileSerializer.new(@user, scope: guardian, root: 'user')
     end
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index 9c451a2b88a2..238aa6bed26b 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -4199,10 +4199,19 @@ RSpec.describe UsersController do
           get "/u/#{admin.username}.json", params: { include_post_count_for: topic.id }
           topic_post_count = response.parsed_body.dig("user", "topic_post_count")
           expect(topic_post_count[topic.id.to_s]).to eq(1)
         end
 
+        it "doesn't include the post count when the signed in user doesn't have access" do
+          c = Fabricate(:category, read_restricted: true)
+          topic.update(category_id: c.id)
+          expect(Guardian.new(user1).can_see?(topic)).to eq(false)
+          get "/u/#{admin.username}.json", params: { include_post_count_for: topic.id }
+          topic_post_count = response.parsed_body.dig("user", "topic_post_count")
+          expect(topic_post_count).to eq(nil)
+        end
+
         it "includes all post types for staff members" do
           SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}"
           sign_in(admin)
 
           get "/u/#{admin.username}.json", params: { include_post_count_for: topic.id }

Vulnerable code:       if topic_id != 0
Fixed code:       if topic_id != 0 && guardian.can_see?(Topic.find_by_id(topic_id))
        it "doesn't include the post count when the signed in user doesn't have access" do
          c = Fabricate(:category, read_restricted: true)
          topic.update(category_id: c.id)
          expect(Guardian.new(user1).can_see?(topic)).to eq(false)
          get "/u/#{admin.username}.json", params: { include_post_count_for: topic.id }
          topic_post_count = response.parsed_body.dig("user", "topic_post_count")
          expect(topic_post_count).to eq(nil)
        end