CVE-2022-24782 (Ruby, published 2022-03-24T21:15Z)
Description: value: Discourse is an open source discussion platform. Versions 2.8.2 and prior in the `stable` branch, 2.9.0.beta3 and prior in the `beta` branch, and 2.9.0.beta3 and prior in the `tests-passed` branch are vulnerable to a data leak. Users can request an export of their own activity. Sometimes, due to category settings, they may have category membership for a secure category. The name of this secure category is shown to the user in the export. The same thing occurs when the user's post has been moved to a secure category. A patch for this issue is available in the `main` branch of Discourse's GitHub repository and is anticipated to be part of future releases.
Severity: MEDIUM (CVSS v3 base score: 4.3)
Weakness: CWE-200 \u2014 Exposure of Sensitive Information to an Unauthorized Actor
The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
Repository: https://github.com/discourse/discourse
Files changed:
app/jobs/regular/export_user_archive.rb
spec/jobs/export_user_archive_spec.rb
Commit message: SECURITY: Hide private categories in user activity export (#16273)

In some of the user's own activity export data,
we sometimes showed a secure category's name or
exposed the existence of a secure category.

Vulnerable code:
        .select(:category_id, :notification_level, :last_seen_at)
        yield [
          cu.category_id,
          piped_category_name(cu.category_id),
          NotificationLevels.all[cu.notification_level],
          cu.last_seen_at
        ]
      end
    def piped_category_name(category_id)
      return "-" unless category_id
      category = Category.find_by(id: category_id)
      return "#{category_id}" unless category
      topic_data = Topic.with_deleted.find_by(id: user_archive['topic_id']) if topic_data.nil?
      categories = piped_category_name(topic_data.category_id)
  fab!(:category) { Fabricate(:category_with_definition) }
    let(:subsubcategory) { Fabricate(:category_with_definition, parent_category_id: subcategory.id) }
    let(:announcements) { Fabricate(:category_with_definition) }
    let(:deleted_category) { Fabricate(:category) }
    it 'correctly exports the CategoryUser table' do
      expect(data.find { |r| r['category_id'] == category.id }).to be_nil
      expect(data.length).to eq(4)
      expect(data[3][:category_names]).to eq(data[3][:category_id])

Fixed code:
        .includes(:category)
        .merge(Category.secured(guardian))
          yield [
            cu.category_id,
            piped_category_name(cu.category_id, cu.category),
            NotificationLevels.all[cu.notification_level],
            cu.last_seen_at
          ]
        end
    def piped_category_name(category_id, category)
      return "#{category_id}" if category_id && !category
      return "-" if !guardian.can_see_category?(category)
      topic_data = Topic.with_deleted.includes(:category).find_by(id: user_archive['topic_id']) if topic_data.nil?
      categories = piped_category_name(topic_data.category_id, topic_data.category)
  fab!(:category) { Fabricate(:category_with_definition, name: "User Archive Category") }

    it "can export a post from a secure category, obscuring the category name" do
      cat2 = Fabricate(:private_category, group: Fabricate(:group), name: "Secret Cat")
      topic2 = Fabricate(:topic, category: cat2, user: user, title: "This is a test secure topic")
      _post2 = Fabricate(:post, topic: topic2, user: user)
      data, csv_out = make_component_csv
      expect(csv_out).not_to match "Secret Cat"
      expect(data.length).to eq(1)
      expect(data[0][:topic_title]).to eq("This is a test secure topic")
      expect(data[0][:categories]).to eq("-")
    end
    let(:subsubcategory) { Fabricate(:category_with_definition, parent_category_id: subcategory.id, name: "User Archive Subcategory") }
    let(:announcements) { Fabricate(:category_with_definition, name: "Announcements") }
    let(:deleted_category) { Fabricate(:category, name: "Deleted Category") }
    let(:secure_category_group) { Fabricate(:group) }
    let(:secure_category) { Fabricate(:private_category, group: secure_category_group, name: "Super Secret Category") }
    it 'correctly exports the CategoryUser table, excluding deleted categories' do
      expect(data.find { |r| r['category_id'] == category.id.to_s }).to be_nil
      expect(data.find { |r| r['category_id'] == deleted_category.id.to_s }).to be_nil
      expect(data.length).to eq(3)
    end
    it "does not include any secure categories the user does not have access to, even if the user has a CategoryUser record" do
      CategoryUser.set_notification_level_for_category(user, NotificationLevels.all[:muted], secure_category.id)
      data, _csv_out = make_component_csv

      expect(data.any? { |r| r['category_id'] == secure_category.id.to_s }).to eq(false)
      expect(data.length).to eq(3)
    end

    it "does include secure categories that the user has access to" do
      CategoryUser.set_notification_level_for_category(user, NotificationLevels.all[:muted], secure_category.id)
      GroupUser.create!(user: user, group: secure_category_group)
      data, _csv_out = make_component_csv

      expect(data.any? { |r| r['category_id'] == secure_category.id.to_s }).to eq(true)
      expect(data.length).to eq(4)