CVE-2021-37703 (Ruby, published 2021-08-13T16:15Z)
Description: value: Discourse is an open-source platform for community discussion. In Discourse before versions 2.7.8 and 2.8.0.beta5, a user's read state for a topic such as the last read post number and the notification level is exposed.
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/models/topic_tracking_state.rb
spec/components/post_creator_spec.rb
spec/models/topic_tracking_state_spec.rb
Commit message: SECURITY: User's read state for topic is leaked to unauthorized clients.

A user's read state for a topic such as the last read post number and the notification level is exposed.


Vulnerable code:
    tags = nil
    tag_ids = nil
    if include_tags_in_report?
      tag_ids, tags = post.topic.tags.pluck(:id, :name).transpose
    TopicUser
      .tracking(post.topic_id)
      .includes(user: :user_stat)
      MessageBus.publish(self.unread_channel_key(tu.user_id), message.as_json, group_ids: group_ids)
        admin = Fabricate(:admin)

Fixed code:
    tags = nil
    tag_ids = nil
    if include_tags_in_report?
      tag_ids, tags = post.topic.tags.pluck(:id, :name).transpose
    end

    scope = TopicUser
      .tracking(post.topic_id)
      .includes(user: :user_stat)
    if group_ids.present?
      scope = scope
        .joins("INNER JOIN group_users gu ON gu.user_id = topic_users.user_id")
        .where("gu.group_id IN (?)", group_ids)
    scope
      MessageBus.publish(self.unread_channel_key(tu.user_id), message.as_json,
        user_ids: [tu.user_id]
      )
        admin = Fabricate(:user)
        admin.grant_admin!
      expect(message.user_ids).to contain_exactly(post.user.id)
      expect(message.group_ids).to eq(nil)
    it "does not publish whisper post to non-staff users" do
      post.update!(post_type: Post.types[:whisper])

      messages = MessageBus.track_publish(described_class.unread_channel_key(post.user_id)) do
        TopicTrackingState.publish_unread(post)
      end

      expect(messages).to eq([])

      post.user.grant_admin!

      message = MessageBus.track_publish(described_class.unread_channel_key(post.user_id)) do
        TopicTrackingState.publish_unread(post)
      end.first

      expect(message.user_ids).to contain_exactly(post.user_id)
      expect(message.group_ids).to eq(nil)
    end

    it "correctly publishes unread for a post in a restricted category" do
      group = Fabricate(:group)
      category = Fabricate(:private_category, group: group)

      post.topic.update!(category: category)

      messages = MessageBus.track_publish(described_class.unread_channel_key(post.user_id)) do
        TopicTrackingState.publish_unread(post)
      end

      expect(messages).to eq([])

      group.add(post.user)

      message = MessageBus.track_publish(described_class.unread_channel_key(post.user_id)) do
        TopicTrackingState.publish_unread(post)
      end.first

      expect(message.user_ids).to contain_exactly(post.user_id)
      expect(message.group_ids).to eq(nil)
    end