SECURITY: Filter private subcategory counts from category listings (#41384)

The category list aggregate topic counts included direct subcategories
without
checking whether the current guardian could see them.

Before we the only possible information that could be gotten was:

  - private subcategory topic counts
  - recency buckets for topic creation: day/week/month/year/all-time

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
This commit is contained in:
Gabriel Grubba
2026-07-03 14:40:52 -04:00
committed by GitHub
parent 705f02a9df
commit 827f20a1a0
2 changed files with 38 additions and 1 deletions
@@ -57,7 +57,11 @@ class CategoryDetailedSerializer < BasicCategorySerializer
def count_with_subcategories(method)
count = object.public_send(method) || 0
object.subcategories.each { |category| count += category.public_send(method) || 0 }
object.subcategories.each do |category|
next if !scope.can_see_category?(category)
count += category.public_send(method) || 0
end
count
end
@@ -83,6 +83,39 @@ RSpec.describe CategoriesController do
expect(subcategories_for_category).to eq(nil)
end
it "excludes private subcategory counts for anonymous users", :aggregate_failures do
private_subcategory = Fabricate(:category, user: admin, parent_category: category)
private_subcategory.set_permissions(admins: :full)
private_subcategory.save!
Fabricate.times(7, :topic, category: private_subcategory)
Category.update_stats
get "/categories.json"
expect(response).to have_http_status(:ok)
category_list = response.parsed_body["category_list"]
category_response =
category_list["categories"].find { |category_json| category_json["id"] == category.id }
expect(category_response["subcategory_ids"]).not_to include(private_subcategory.id)
expect(
category_response.slice(
"topics_all_time",
"topics_year",
"topics_month",
"topics_week",
"topics_day",
),
).to eq(
"topics_all_time" => 0,
"topics_year" => 0,
"topics_month" => 0,
"topics_week" => 0,
"topics_day" => 0,
)
end
it "returns the right subcategory response with permission" do
subcategory = Fabricate(:category, user: admin, parent_category: category)