CVE-2026-47264
Description
Discourse versions prior to 2026.1.4, 2026.3.1, and 2026.4.1 expose restricted tag group names to anonymous users via the tag info endpoint.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Discourse versions prior to 2026.1.4, 2026.3.1, and 2026.4.1 expose restricted tag group names to anonymous users via the tag info endpoint.
Vulnerability
In Discourse, the DetailedTagSerializer#tag_group_names method returned all tag groups a tag belonged to without filtering against the requesting user's visibility. This affects versions 2026.1.0-latest to before 2026.1.4, 2026.3.0-latest to before 2026.3.1, and 2026.4.0-latest to before 2026.4.1. The issue is exploitable only when SiteSetting.tags_listed_by_group is enabled [1].
Exploitation
An anonymous or unprivileged user can access the TagsController#info endpoint, which is exempt from the requires_login check. By sending a request to this endpoint for a tag that belongs to restricted tag groups, the response includes the names of those groups without any access control [1].
Impact
Successful exploitation allows the attacker to read the names of tag groups that are restricted to specific user groups or non-visible categories. This constitutes an information disclosure of potentially sensitive group names [1].
Mitigation
The issue has been patched in versions 2026.1.4, 2026.3.1, 2026.4.1, and 2026.5.0-latest.1. Users should upgrade to one of these patched versions. No workaround is available other than upgrading [1].
AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2>=2026.4.0,<2026.4.1+ 1 more
- (no CPE)range: >=2026.4.0,<2026.4.1
- (no CPE)range: >=2026.1.0 <2026.1.4 || >=2026.3.0 <2026.3.1 || >=2026.4.0 <2026.4.1
Patches
353f8519bed57SECURITY: Don't leak restricted tag group names via tag info [backport 2026.4]
2 files changed · +38 −1
app/serializers/detailed_tag_serializer.rb+4 −1 modified@@ -22,6 +22,9 @@ def include_tag_group_names? end def tag_group_names - object.tag_groups.map(&:name) + TagGroup + .visible(scope) + .where(id: object.tag_group_memberships.select(:tag_group_id)) + .pluck(:name) end end
spec/requests/tags_controller_spec.rb+34 −0 modified@@ -706,6 +706,40 @@ expect(response.parsed_body["categories"]).to be_blank expect(response.parsed_body.dig("tag_info", "category_restricted")).to eq(true) end + + it "doesn't leak the restricted tag group name to users without access" do + SiteSetting.tags_listed_by_group = true + sign_in(user) + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq([]) + end + + it "doesn't leak the restricted tag group name to anon" do + SiteSetting.tags_listed_by_group = true + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq([]) + end + + it "still returns the restricted tag group name to admins" do + SiteSetting.tags_listed_by_group = true + sign_in(admin) + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq([tag_group.name]) + end + + it "returns only visible tag group names when tag is in multiple groups" do + SiteSetting.tags_listed_by_group = true + public_tag_group = Fabricate(:tag_group, name: "public-group", tags: [tag]) + sign_in(user) + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq( + [public_tag_group.name], + ) + end end end end
4d4f411e3d55SECURITY: Don't leak restricted tag group names via tag info [backport 2026.3]
2 files changed · +38 −1
app/serializers/detailed_tag_serializer.rb+4 −1 modified@@ -22,6 +22,9 @@ def include_tag_group_names? end def tag_group_names - object.tag_groups.map(&:name) + TagGroup + .visible(scope) + .where(id: object.tag_group_memberships.select(:tag_group_id)) + .pluck(:name) end end
spec/requests/tags_controller_spec.rb+34 −0 modified@@ -688,6 +688,40 @@ expect(response.parsed_body["categories"]).to be_blank expect(response.parsed_body.dig("tag_info", "category_restricted")).to eq(true) end + + it "doesn't leak the restricted tag group name to users without access" do + SiteSetting.tags_listed_by_group = true + sign_in(user) + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq([]) + end + + it "doesn't leak the restricted tag group name to anon" do + SiteSetting.tags_listed_by_group = true + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq([]) + end + + it "still returns the restricted tag group name to admins" do + SiteSetting.tags_listed_by_group = true + sign_in(admin) + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq([tag_group.name]) + end + + it "returns only visible tag group names when tag is in multiple groups" do + SiteSetting.tags_listed_by_group = true + public_tag_group = Fabricate(:tag_group, name: "public-group", tags: [tag]) + sign_in(user) + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq( + [public_tag_group.name], + ) + end end end end
be790bdcc04bSECURITY: Don't leak restricted tag group names via tag info [backport 2026.1]
2 files changed · +38 −1
app/serializers/detailed_tag_serializer.rb+4 −1 modified@@ -22,6 +22,9 @@ def include_tag_group_names? end def tag_group_names - object.tag_groups.map(&:name) + TagGroup + .visible(scope) + .where(id: object.tag_group_memberships.select(:tag_group_id)) + .pluck(:name) end end
spec/requests/tags_controller_spec.rb+34 −0 modified@@ -688,6 +688,40 @@ expect(response.parsed_body["categories"]).to be_blank expect(response.parsed_body.dig("tag_info", "category_restricted")).to eq(true) end + + it "doesn't leak the restricted tag group name to users without access" do + SiteSetting.tags_listed_by_group = true + sign_in(user) + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq([]) + end + + it "doesn't leak the restricted tag group name to anon" do + SiteSetting.tags_listed_by_group = true + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq([]) + end + + it "still returns the restricted tag group name to admins" do + SiteSetting.tags_listed_by_group = true + sign_in(admin) + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq([tag_group.name]) + end + + it "returns only visible tag group names when tag is in multiple groups" do + SiteSetting.tags_listed_by_group = true + public_tag_group = Fabricate(:tag_group, name: "public-group", tags: [tag]) + sign_in(user) + get "/tag/#{tag.name}/info.json" + expect(response.status).to eq(200) + expect(response.parsed_body.dig("tag_info", "tag_group_names")).to eq( + [public_tag_group.name], + ) + end end end end
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
1News mentions
0No linked articles in our index yet.