CVE-2022-21677: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: Discourse is an open source discussion platform. Discourse groups can be configured with varying visibility levels for the group as well as the group members. By default, a newly created group has its visibility set to public and the group's members visibility set to public as well. However, a group's visibility and the group's members visibility can be configured such that it is restricted to logged on users, members of the group or staff users. A vulnerability has been discovered in versions prior to 2.7.13 and 2.8.0.beta11 where the group advanced search option does not respect the group's visibility and members visibility level. As such, a group with restricted visibility or members visibility can be revealed through search with the right search option. This issue is patched in `stable` version 2.7.13, `beta` version 2.8.0.beta11, and `tests-passed` version 2.8.0.beta11 versions of Discourse. There are no workarounds aside from upgrading.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 5.3
Published: 2022-01-14T17:15Z
Repository: https://github.com/discourse/discourse
Language: Ruby
Files changed: lib/search.rb, spec/components/search_spec.rb
Commit date: 2022-01-10 13:49:26 +0800
Commit message: SECURITY: Advanced group search did not respect visiblity of groups.

Diff: diff --git a/lib/search.rb b/lib/search.rb
index c5abbc503ea..4a3ab96da1e 100644
--- a/lib/search.rb
+++ b/lib/search.rb
@@ -581,11 +581,15 @@ class Search
       end
     end
   end
 
   advanced_filter(/^group:(.+)$/i) do |posts, match|
-    group_id = Group.where('name ilike ? OR (id = ? AND id > 0)', match, match.to_i).pluck_first(:id)
+    group_id = Group
+      .visible_groups(@guardian.user)
+      .members_visible_groups(@guardian.user)
+      .where('name ilike ? OR (id = ? AND id > 0)', match, match.to_i).pluck_first(:id)
+
     if group_id
       posts.where("posts.user_id IN (select gu.user_id from group_users gu where gu.group_id = ?)", group_id)
     else
       posts.where("1 = 0")
     end
diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb
index 34947fbc24f..f47ca2438ea 100644
--- a/spec/components/search_spec.rb
+++ b/spec/components/search_spec.rb
@@ -1246,19 +1246,59 @@ describe Search do
       expect(Search.execute("user:#{post_1.user_id}").posts).to contain_exactly(post_1)
 
       expect(Search.execute("@#{post_1.user.username}").posts).to contain_exactly(post_1)
     end
 
-    it 'supports group' do
-      topic = Fabricate(:topic, created_at: 3.months.ago)
-      post = Fabricate(:post, raw: 'hi this is a test 123 123', topic: topic)
+    context "searching for posts made by users of a group" do
+      fab!(:topic) { Fabricate(:topic, created_at: 3.months.ago) }
+      fab!(:user) { Fabricate(:user) }
+      fab!(:user_2) { Fabricate(:user) }
+      fab!(:user_3) { Fabricate(:user) }
+      fab!(:group) { Fabricate(:group, name: "Like_a_Boss").tap { |g| g.add(user) } }
+      fab!(:group_2) { Fabricate(:group).tap { |g| g.add(user_2) } }
+      let!(:post) { Fabricate(:post, raw: 'hi this is a test 123 123', topic: topic, user: user) }
+      let!(:post_2) { Fabricate(:post, user: user_2) }
+
+      it 'should not return any posts if group does not exist' do
+        group.update!(
+          visibility_level: Group.visibility_levels[:public],
+          members_visibility_level: Group.visibility_levels[:public]
+        )
+
+        expect(Search.execute('group:99999').posts).to eq([])
+      end
+
+      it 'should return the right posts for a public group' do
+        group.update!(
+          visibility_level: Group.visibility_levels[:public],
+          members_visibility_level: Group.visibility_levels[:public]
+        )
 
-      group = Group.create!(name: "Like_a_Boss")
-      GroupUser.create!(user_id: post.user_id, group_id: group.id)
+        expect(Search.execute('group:like_a_boss').posts).to contain_exactly(post)
+        expect(Search.execute("group:#{group.id}").posts).to contain_exactly(post)
+      end
 
-      expect(Search.execute('group:like_a_boss').posts.length).to eq(1)
-      expect(Search.execute('group:"like a brick"').posts.length).to eq(0)
+      it "should return the right posts for a public group with members' visibility restricted to logged on users" do
+        group.update!(
+          visibility_level: Group.visibility_levels[:public],
+          members_visibility_level: Group.visibility_levels[:logged_on_users]
+        )
+
+        expect(Search.execute("group:#{group.id}").posts).to eq([])
+        expect(Search.execute("group:#{group.id}", guardian: Guardian.new(user_3)).posts).to contain_exactly(post)
+      end
+
+      it "should return the right posts for a group with visibility restricted to logged on users with members' visibility restricted to members" do
+        group.update!(
+          visibility_level: Group.visibility_levels[:logged_on_users],
+          members_visibility_level: Group.visibility_levels[:members]
+        )
+
+        expect(Search.execute("group:#{group.id}").posts).to eq([])
+        expect(Search.execute("group:#{group.id}", guardian: Guardian.new(user_3)).posts).to eq([])
+        expect(Search.execute("group:#{group.id}", guardian: Guardian.new(user)).posts).to contain_exactly(post)
+      end
     end
 
     it 'supports badge' do
 
       topic = Fabricate(:topic, created_at: 3.months.ago)

Vulnerable code:     group_id = Group.where('name ilike ? OR (id = ? AND id > 0)', match, match.to_i).pluck_first(:id)
    it 'supports group' do
      topic = Fabricate(:topic, created_at: 3.months.ago)
      post = Fabricate(:post, raw: 'hi this is a test 123 123', topic: topic)
      group = Group.create!(name: "Like_a_Boss")
      GroupUser.create!(user_id: post.user_id, group_id: group.id)
      expect(Search.execute('group:like_a_boss').posts.length).to eq(1)
      expect(Search.execute('group:"like a brick"').posts.length).to eq(0)
Fixed code:     group_id = Group
      .visible_groups(@guardian.user)
      .members_visible_groups(@guardian.user)
      .where('name ilike ? OR (id = ? AND id > 0)', match, match.to_i).pluck_first(:id)

    context "searching for posts made by users of a group" do
      fab!(:topic) { Fabricate(:topic, created_at: 3.months.ago) }
      fab!(:user) { Fabricate(:user) }
      fab!(:user_2) { Fabricate(:user) }
      fab!(:user_3) { Fabricate(:user) }
      fab!(:group) { Fabricate(:group, name: "Like_a_Boss").tap { |g| g.add(user) } }
      fab!(:group_2) { Fabricate(:group).tap { |g| g.add(user_2) } }
      let!(:post) { Fabricate(:post, raw: 'hi this is a test 123 123', topic: topic, user: user) }
      let!(:post_2) { Fabricate(:post, user: user_2) }

      it 'should not return any posts if group does not exist' do
        group.update!(
          visibility_level: Group.visibility_levels[:public],
          members_visibility_level: Group.visibility_levels[:public]
        )

        expect(Search.execute('group:99999').posts).to eq([])
      end

      it 'should return the right posts for a public group' do
        group.update!(
          visibility_level: Group.visibility_levels[:public],
          members_visibility_level: Group.visibility_levels[:public]
        )
        expect(Search.execute('group:like_a_boss').posts).to contain_exactly(post)
        expect(Search.execute("group:#{group.id}").posts).to contain_exactly(post)
      end
      it "should return the right posts for a public group with members' visibility restricted to logged on users" do
        group.update!(
          visibility_level: Group.visibility_levels[:public],
          members_visibility_level: Group.visibility_levels[:logged_on_users]
        )

        expect(Search.execute("group:#{group.id}").posts).to eq([])
        expect(Search.execute("group:#{group.id}", guardian: Guardian.new(user_3)).posts).to contain_exactly(post)
      end

      it "should return the right posts for a group with visibility restricted to logged on users with members' visibility restricted to members" do
        group.update!(
          visibility_level: Group.visibility_levels[:logged_on_users],
          members_visibility_level: Group.visibility_levels[:members]
        )

        expect(Search.execute("group:#{group.id}").posts).to eq([])
        expect(Search.execute("group:#{group.id}", guardian: Guardian.new(user_3)).posts).to eq([])
        expect(Search.execute("group:#{group.id}", guardian: Guardian.new(user)).posts).to contain_exactly(post)
      end