CVE-2021-41140 (Ruby, published 2021-10-19T18:15Z)
Description: value: Discourse-reactions is a plugin for the Discourse platform that allows user to add their reactions to the post. In affected versions reactions given by user to secure topics and private messages are visible. This issue is patched in version 0.2 of discourse-reaction. Users who are unable to update are advised to disable the Discourse-reactions plugin in admin panel.
Severity: MEDIUM (CVSS v3 base score: 5.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-reactions
Files changed:
app/controllers/discourse_reactions/custom_reactions_controller.rb
spec/requests/custom_reactions_controller_spec.rb
Commit message: SECURITY: leaking PMs and secure categories topics (#110)

Ensure that user has access to private messages and categories of he topics

Vulnerable code:
        .joins(:reaction, :post)

Fixed code:
        .joins(:reaction, post: :topic)
        .joins("LEFT JOIN categories ON categories.id = topics.category_id")
      reaction_users = secure_reaction_users!(reaction_users)


    def secure_reaction_users!(reaction_users)
      if !guardian.can_see_private_messages?(current_user.id) || !guardian.user
        reaction_users = reaction_users.where("topics.archetype <> :private_message", private_message: archetype::private_message)
      else
        unless guardian.is_admin?
          sql = <<~SQL
            topics.archetype <> :private_message OR
            EXISTS (
              SELECT 1 FROM topic_allowed_users tu WHERE tu.topic_id = topics.id AND tu.user_id = :current_user_id
            ) OR
            EXISTS (
              SELECT 1 FROM topic_allowed_groups tg WHERE tg.topic_id = topics.id AND tg.group_id IN (
                SELECT group_id FROM group_users gu WHERE gu.user_id = :current_user_id
              )
            )
          SQL

          reaction_users = reaction_users.where(sql, private_message: Archetype::private_message, current_user_id: guardian.user.id)
        end
      end

      unless guardian.is_admin?
        allowed = guardian.secure_category_ids
        if allowed.present?
          reaction_users = reaction_users.where("(categories.read_restricted IS NULL OR
                         NOT categories.read_restricted OR
                        (categories.read_restricted and categories.id in (:categories)) )", categories: guardian.secure_category_ids)
        else
          reaction_users = reaction_users.where("(categories.read_restricted IS NULL OR NOT categories.read_restricted)")
        end
      end

      reaction_users
    end
require_relative '../fabricators/reaction_fabricator.rb'
require_relative '../fabricators/reaction_user_fabricator.rb'
    fab!(:private_topic) { Fabricate(:private_message_topic, user: user_2) }
    fab!(:private_post) { Fabricate(:post, topic: private_topic) }
    fab!(:secure_group) { Fabricate(:group) }
    fab!(:secure_category) { Fabricate(:private_category, group: secure_group) }
    fab!(:secure_topic) { Fabricate(:topic, category: secure_category) }
    fab!(:secure_post) { Fabricate(:post, topic: secure_topic) }
    fab!(:private_reaction) { Fabricate(:reaction, post: private_post, reaction_value: "hugs") }
    fab!(:secure_reaction) { Fabricate(:reaction, post: secure_post, reaction_value: "hugs") }
    fab!(:private_topic_reaction_user) { Fabricate(:reaction_user, reaction: private_reaction, user: user_2, post: private_post) }
    fab!(:secure_topic_reaction_user) { Fabricate(:reaction_user, reaction: secure_reaction, user: user_2, post: secure_post) }

    it 'does not return reactions for private messages' do
      sign_in(user_1)

      get "/discourse-reactions/posts/reactions.json", params: { username: user_2.username }
      parsed = response.parsed_body
      expect(response.parsed_body.map { |reaction| reaction["post_id"] }).not_to include(private_post.id)

      sign_in(user_2)

      get "/discourse-reactions/posts/reactions.json", params: { username: user_2.username }
      parsed = response.parsed_body
      expect(response.parsed_body.map { |reaction| reaction["post_id"] }).to include(private_post.id)
    end

    it 'does not return reactions for secure categories' do
      secure_group.add(user_2)
      sign_in(user_1)

      get "/discourse-reactions/posts/reactions.json", params: { username: user_2.username }
      parsed = response.parsed_body
      expect(response.parsed_body.map { |reaction| reaction["post_id"] }).not_to include(secure_post.id)

      secure_group.add(user_1)
      get "/discourse-reactions/posts/reactions.json", params: { username: user_2.username }
      parsed = response.parsed_body
      expect(response.parsed_body.map { |reaction| reaction["post_id"] }).to include(secure_post.id)

      sign_in(user_2)

      get "/discourse-reactions/posts/reactions.json", params: { username: user_2.username }
      parsed = response.parsed_body
      expect(response.parsed_body.map { |reaction| reaction["post_id"] }).to include(secure_post.id)
    end