CVE-2022-41944 (published 2022-11-28T15:15Z)

Discourse is an open-source discussion platform. In stable versions prior to 2.8.12 and beta or tests-passed versions prior to 2.9.0.beta.13, under certain conditions, a user can see notifications for topics they no longer have access to. If there is sensitive information in the topic title, it will therefore have been exposed. This issue is patched in stable version 2.8.12, beta version 2.9.0.beta13, and tests-passed version 2.9.0.beta13. There are no workarounds available.

CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N)

Weakness information:
cwe_id: CWE-863
cwe_name: Incorrect Authorization
description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are incorrectly applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/discourse/discourse
commit_details: repo_url: https://github.com/discourse/discourse
author: Martin Brennan
author_date: 2022-11-28 10:41:57+10:00
msg: SECURITY: Hide notifications for inaccessible topics (#19208)

Filter notifications the user cannot see anymore
via guardian.can_see_topic_ids
repository: repo_url: https://github.com/discourse/discourse
repo_name: discourse/discourse
description: A platform for community discussion. Free, open, simple.
repo_language: Ruby
owner: discourse
file_changes: filename: notifications_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -40,7 +40,7 @@ class NotificationsController < ApplicationController
       end
 
       if notifications.present? && !(params.has_key?(:silent) || @readonly_mode)
-        if changed = current_user.bump_last_seen_notification!
+        if current_user.bump_last_seen_notification!
           current_user.reload
           current_user.publish_notifications_state
         end
@@ -57,6 +57,8 @@ class NotificationsController < ApplicationController
         end
       end
 
+      notifications = filter_inaccessible_notifications(notifications)
+
       json = {
         notifications: serialize_data(notifications, NotificationSerializer),
         seen_notification_id: current_user.seen_notification_id
@@ -82,6 +84,7 @@ class NotificationsController < ApplicationController
 
       total_rows = notifications.dup.count
       notifications = notifications.offset(offset).limit(60)
+      notifications = filter_inaccessible_notifications(notifications)
       render_json_dump(notifications: serialize_data(notifications, NotificationSerializer),
                        total_rows_notifications: total_rows,
                        seen_notification_id: user.seen_notification_id,
@@ -145,4 +148,9 @@ class NotificationsController < ApplicationController
     render_json_dump(NotificationSerializer.new(@notification, scope: guardian, root: false))
   end
 
+  def filter_inaccessible_notifications(notifications)
+    topic_ids = notifications.map { |n| n.topic_id }.compact.uniq
+    accessible_topic_ids = guardian.can_see_topic_ids(topic_ids: topic_ids)
+    notifications.select { |n| n.topic_id.blank? || accessible_topic_ids.include?(n.topic_id) }
+  end
 end

programming_language: Ruby
filename: notifications_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -339,6 +339,50 @@ RSpec.describe NotificationsController do
             expect(response.status).to eq(404)
           end
         end
+
+        context "with notifications for inaccessible topics" do
+          fab!(:sender) { Fabricate.build(:topic_allowed_user, user: Fabricate(:coding_horror)) }
+          fab!(:allowed_user) { Fabricate.build(:topic_allowed_user, user: user) }
+          fab!(:another_allowed_user) { Fabricate.build(:topic_allowed_user, user: Fabricate(:user)) }
+          fab!(:allowed_pm) { Fabricate(:private_message_topic, topic_allowed_users: [sender, allowed_user, another_allowed_user]) }
+          fab!(:forbidden_pm) { Fabricate(:private_message_topic, topic_allowed_users: [sender, another_allowed_user]) }
+          fab!(:allowed_pm_notification) { Fabricate(:private_message_notification, user: user, topic: allowed_pm) }
+          fab!(:forbidden_pm_notification) { Fabricate(:private_message_notification, user: user, topic: forbidden_pm) }
+
+          def expect_correct_notifications(response)
+            notification_ids = response.parsed_body["notifications"].map { |n| n["id"] }
+            expect(notification_ids).to include(allowed_pm_notification.id)
+            expect(notification_ids).to_not include(forbidden_pm_notification.id)
+          end
+
+          context "with 'recent' filter" do
+            it "doesn't include notifications from topics the user isn't allowed to see" do
+              SiteSetting.enable_experimental_sidebar_hamburger = true
+              get "/notifications.json", params: { recent: true }
+              expect(response.status).to eq(200)
+              expect_correct_notifications(response)
+
+              SiteSetting.enable_experimental_sidebar_hamburger = false
+              get "/notifications.json", params: { recent: true }
+              expect(response.status).to eq(200)
+              expect_correct_notifications(response)
+            end
+          end
+
+          context "without 'recent' filter" do
+            it "doesn't include notifications from topics the user isn't allowed to see" do
+              SiteSetting.enable_experimental_sidebar_hamburger = true
+              get "/notifications.json"
+              expect(response.status).to eq(200)
+              expect_correct_notifications(response)
+
+              SiteSetting.enable_experimental_sidebar_hamburger = false
+              get "/notifications.json"
+              expect(response.status).to eq(200)
+              expect_correct_notifications(response)
+            end
+          end
+        end
       end
 
       it 'should succeed' do

programming_language: Ruby