CVE-2023-23935 (JavaScript, published 2023-03-16T21:15Z)
Description: value: Discourse is an open-source messaging platform. In versions 3.0.1 and prior on the `stable` branch and versions 3.1.0.beta2 and prior on the `beta` and `tests-passed` branches, the count of personal messages displayed for a tag is a count of all personal messages regardless of whether the personal message is visible to a given user. As a result, any users can technically poll a sensitive tag to determine if a new personal message is created even if the user does not have access to the personal message.

In the patched versions, the count of personal messages tagged with a given tag is hidden by default. To revert to the old behaviour of displaying the count of personal messages for a given tag, an admin may enable the `display_personal_messages_tag_counts` site setting.
Severity: nan (CVSS v3 base score: 4.3)
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/discourse/discourse
Files changed:
app/assets/javascripts/discourse/app/models/tag.js
app/assets/javascripts/discourse/tests/helpers/create-pretender.js
app/assets/javascripts/discourse/tests/integration/components/select-kit/tag-drop-test.js
app/assets/javascripts/discourse/tests/unit/models/tag-test.js
app/controllers/tags_controller.rb
config/locales/server.en.yml
config/site_settings.yml
spec/requests/tags_controller_spec.rb
Commit message: SECURITY: Hide PM count for tags by default (#20061)

Currently `Topic#pm_topic_count` is a count of all personal messages tagged for a given tag. As a result, any user with access to PM tags can poll a sensitive tag to determine if a new personal message has been created using that tag even if the user does not have access to the personal message. We classify this as a minor leak in sensitive information.

With this commit, `Topic#pm_topic_count` is hidden from users by default unless the `display_personal_messages_tag_counts` site setting is enabled. 

Vulnerable code:
  @discourseComputed("count", "pm_count")
  totalCount(count, pmCount) {
    return count + pmCount;
  },
  pmOnly(count, pmCount) {
    return count === 0 && pmCount > 0;
        { id: "planned", text: "planned", count: 7, pm_count: 0 },
        { id: "private", text: "private", count: 0, pm_count: 7 },
              { id: "Escort", text: "Escort", count: 1, pm_count: 0 },
              { id: "focus", text: "focus", count: 3, pm_count: 0 },
              { id: "civic", text: "civic", count: 4, pm_count: 0 },
              { id: "accord", text: "accord", count: 2, pm_count: 0 },
              { id: "ford", text: "ford", count: 5, pm_count: 0 },
              { id: "honda", text: "honda", count: 6, pm_count: 0 },
          results: [{ id: "David", name: "David", count: 2, pm_count: 0 }],
        {
          pm_count: show_pm_tags ? t.pm_topic_count : 0,
      Fabricate(:tag, name: "topic-test", public_topic_count: 1, staff_topic_count: 1)
        expect(tags[0]["count"]).to eq(0)
        expect(tags[0]["pm_count"]).to eq(0)
        expect(tags[1]["count"]).to eq(1)
        expect(tags[1]["pm_count"]).to eq(0)
        expect(tags[0]["pm_count"]).to eq(0)
          expect(tags.length).to eq(2)
          expect(serialized_tag["pm_count"]).to eq(0)
        expect(tags[0]["pm_count"]).to eq(0)
        expect(categories[0]["tags"][0]["pm_count"]).to eq(0)

Fixed code:
import { readOnly } from "@ember/object/computed";
  pmOnly: readOnly("pm_only"),
  totalCount(count, pmCount) {
    return pmCount ? count + pmCount : count;
        { id: "planned", text: "planned", count: 7, pm_only: false },
        { id: "private", text: "private", count: 0, pm_only: true },
              { id: "Escort", text: "Escort", count: 1, pm_only: false },
              { id: "focus", text: "focus", count: 3, pm_only: false },
              { id: "civic", text: "civic", count: 4, pm_only: false },
              { id: "accord", text: "accord", count: 2, pm_only: false },
              { id: "ford", text: "ford", count: 5, pm_only: false },
              { id: "honda", text: "honda", count: 6, pm_only: false },
          results: [{ id: "David", name: "David", count: 2, pm_only: false }],
import { module, test } from "qunit";
import { getOwner } from "discourse-common/lib/get-owner";
import { setupTest } from "ember-qunit";

module("Unit | Model | tag", function (hooks) {
  setupTest(hooks);

  hooks.beforeEach(function () {
    this.store = getOwner(this).lookup("service:store");
  });

  test("totalCount when pm_count is not present", function (assert) {
    const tag = this.store.createRecord("tag", { count: 5 });
    assert.strictEqual(tag.totalCount, 5);
  });

  test("totalCount when pm_count is present", function (assert) {
    const tag = this.store.createRecord("tag", { count: 5, pm_count: 8 });
    assert.strictEqual(tag.totalCount, 13);
  });

  test("pmOnly", function (assert) {
    const tag = this.store.createRecord("tag", { pm_only: false });

    assert.notOk(tag.pmOnly);

    tag.set("pm_only", true);

    assert.ok(tag.pmOnly);
  });
});
        attrs = {
          pm_only: topic_count == 0 && t.pm_topic_count > 0,

        if show_pm_tags && SiteSetting.display_personal_messages_tag_counts
          attrs[:pm_count] = t.pm_topic_count
        end

        attrs
    display_personal_messages_tag_counts: "When enabled, count of personal messages tagged with a given tag will be displayed."
  display_personal_messages_tag_counts:
    default: false

      Fabricate(
        :tag,
        name: "topic-test",
        public_topic_count: 1,
        staff_topic_count: 1,
        pm_topic_count: 5,
      )
    end

    fab!(:pm_only_tag) do
      Fabricate(:tag, public_topic_count: 0, staff_topic_count: 0, pm_topic_count: 1)

        serialized_tag = tags.find { |t| t["id"] == test_tag.name }

        expect(serialized_tag["count"]).to eq(0)
        expect(serialized_tag["pm_count"]).to eq(nil)
        expect(serialized_tag["pm_only"]).to eq(false)

        serialized_tag = tags.find { |t| t["id"] == topic_tag.name }

        expect(serialized_tag["count"]).to eq(1)
        expect(serialized_tag["pm_count"]).to eq(nil)
        expect(serialized_tag["pm_only"]).to eq(false)
      end

      it "does not include pm_count attribute when user cannot tag PM topics even if display_personal_messages_tag_counts site setting has been enabled" do
        SiteSetting.display_personal_messages_tag_counts = true

        sign_in(admin)

        get "/tags.json"

        expect(response.status).to eq(200)

        tags = response.parsed_body["tags"]

        expect(tags[0]["pm_count"]).to eq(nil)
        expect(tags[1]["pm_count"]).to eq(nil)
      end

      it "includes pm_count attribute when user can tag PM topics and display_personal_messages_tag_counts site setting has been enabled" do
        SiteSetting.display_personal_messages_tag_counts = true
        SiteSetting.pm_tags_allowed_for_groups = Group::AUTO_GROUPS[:admins]

        sign_in(admin)

        get "/tags.json"

        expect(response.status).to eq(200)

        tags = response.parsed_body["tags"]

        serialized_tag = tags.find { |t| t["id"] == test_tag.name }

        expect(serialized_tag["pm_count"]).to eq(0)
        expect(serialized_tag["pm_only"]).to eq(false)

        serialized_tag = tags.find { |t| t["id"] == topic_tag.name }

        expect(serialized_tag["pm_count"]).to eq(5)
        expect(serialized_tag["pm_only"]).to eq(false)

        serialized_tag = tags.find { |t| t["id"] == pm_only_tag.name }

        expect(serialized_tag["pm_count"]).to eq(1)
        expect(serialized_tag["pm_only"]).to eq(true)
        expect(tags[0]["pm_count"]).to eq(nil)
          SiteSetting.display_personal_messages_tag_counts = true
          expect(serialized_tag["pm_count"]).to eq(5)
        expect(tags[0]["pm_count"]).to eq(nil)
        expect(categories[0]["tags"][0]["pm_count"]).to eq(nil)