CVE-2022-21683: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: Wagtail is a Django based content management system focused on flexibility and user experience. When notifications for new replies in comment threads are sent, they are sent to all users who have replied or commented anywhere on the site, rather than only in the relevant threads. This means that a user could listen in to new comment replies on pages they have not have editing access to, as long as they have left a comment or reply somewhere on the site. A patched version has been released as Wagtail 2.15.2, which restores the intended behaviour - to send notifications for new replies to the participants in the active thread only (editing permissions are not considered). New comments can be disabled by setting `WAGTAILADMIN_COMMENTS_ENABLED = False` in the Django settings file.
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: 4.3
Published: 2022-01-18T18:15Z
Repository: https://github.com/wagtail/wagtail
Language: Python
Files changed: wagtail/admin/tests/pages/test_edit_page.py, wagtail/admin/views/pages/edit.py
Commit date: 2022-01-18 10:25:42 +0000
Commit message: Fix commenting thread notifications being sent to non-thread users

Diff: diff --git a/wagtail/admin/tests/pages/test_edit_page.py b/wagtail/admin/tests/pages/test_edit_page.py
index 7eab6481da..2d3e00cace 100644
--- a/wagtail/admin/tests/pages/test_edit_page.py
+++ b/wagtail/admin/tests/pages/test_edit_page.py
@@ -2098,10 +2098,11 @@ class TestCommenting(TestCase, WagtailTestUtils):
 
         # Add a couple more users
         self.subscriber = self.create_user('subscriber')
         self.non_subscriber = self.create_user('non-subscriber')
         self.non_subscriber_2 = self.create_user('non-subscriber-2')
+        self.never_emailed_user = self.create_user('never-emailed')
 
         PageSubscription.objects.create(
             page=self.child_page,
             user=self.user,
             comment_notifications=True
@@ -2111,10 +2112,27 @@ class TestCommenting(TestCase, WagtailTestUtils):
             page=self.child_page,
             user=self.subscriber,
             comment_notifications=True
         )
 
+        # Add comment and reply on a different page for the never_emailed_user
+        # They should never be notified
+        comment_on_other_page = Comment.objects.create(
+            page=self.root_page,
+            user=self.never_emailed_user,
+            text='a comment'
+        )
+
+        CommentReply.objects.create(
+            user=self.never_emailed_user,
+            comment=comment_on_other_page,
+            text='a reply'
+        )
+
+    def assertNeverEmailedWrongUser(self):
+        self.assertNotIn(self.never_emailed_user.email, [to for email in mail.outbox for to in email.to])
+
     def test_new_comment(self):
         post_data = {
             'title': "I've been edited!",
             'content': "Some content",
             'slug': 'hello-world',
@@ -2142,10 +2160,11 @@ class TestCommenting(TestCase, WagtailTestUtils):
         comment = self.child_page.wagtail_admin_comments.get()
         self.assertEqual(comment.text, 'A test comment')
 
         # Check notification email
         self.assertEqual(len(mail.outbox), 1)
+        self.assertNeverEmailedWrongUser()
         self.assertEqual(mail.outbox[0].to, [self.subscriber.email])
         self.assertEqual(mail.outbox[0].subject, 'test@email.com has updated comments on "I\'ve been edited! (simple page)"')
         self.assertIn('New comments:\n - "A test comment"\n\n', mail.outbox[0].body)
 
         # Check audit log
@@ -2281,10 +2300,11 @@ class TestCommenting(TestCase, WagtailTestUtils):
         self.assertTrue(comment.resolved_at)
         self.assertEqual(comment.resolved_by, self.user)
 
         # Check notification email
         self.assertEqual(len(mail.outbox), 2)
+        self.assertNeverEmailedWrongUser()
         # The non subscriber created the comment, so should also get an email
         self.assertEqual(mail.outbox[0].to, [self.non_subscriber.email])
         self.assertEqual(mail.outbox[0].subject, 'test@email.com has updated comments on "I\'ve been edited! (simple page)"')
         self.assertIn('Resolved comments:\n - "A test comment"\n\n', mail.outbox[0].body)
         self.assertEqual(mail.outbox[1].to, [self.subscriber.email])
@@ -2335,10 +2355,11 @@ class TestCommenting(TestCase, WagtailTestUtils):
         # Check the comment was deleted
         self.assertFalse(self.child_page.wagtail_admin_comments.exists())
 
         # Check notification email
         self.assertEqual(len(mail.outbox), 1)
+        self.assertNeverEmailedWrongUser()
         self.assertEqual(mail.outbox[0].to, [self.subscriber.email])
         self.assertEqual(mail.outbox[0].subject, 'test@email.com has updated comments on "I\'ve been edited! (simple page)"')
         self.assertIn('Deleted comments:\n - "A test comment"\n\n', mail.outbox[0].body)
 
         # Check audit log
@@ -2396,10 +2417,11 @@ class TestCommenting(TestCase, WagtailTestUtils):
         comment.refresh_from_db()
         self.assertEqual(comment.replies.last().text, 'a new reply')
 
         # Check notification email
         self.assertEqual(len(mail.outbox), 3)
+        self.assertNeverEmailedWrongUser()
 
         recipients = [mail.to for mail in mail.outbox]
         # The other non subscriber replied in the thread, so should get an email
         self.assertIn([self.non_subscriber_2.email], recipients)
 
diff --git a/wagtail/admin/views/pages/edit.py b/wagtail/admin/views/pages/edit.py
index 2edf11b450..9f942243f2 100644
--- a/wagtail/admin/views/pages/edit.py
+++ b/wagtail/admin/views/pages/edit.py
@@ -139,15 +139,15 @@ class EditView(TemplateResponseMixin, ContextMixin, HookResponseMixin, View):
         global_recipient_users = [subscriber.user for subscriber in subscribers if subscriber.user != self.request.user]
 
         # Get subscribers to individual threads
         replies = CommentReply.objects.filter(comment_id__in=relevant_comment_ids)
         comments = Comment.objects.filter(id__in=relevant_comment_ids)
-        thread_users = get_user_model().objects.exclude(pk=self.request.user.pk).exclude(pk__in=subscribers.values_list('user_id', flat=True)).prefetch_related(
+        thread_users = get_user_model().objects.exclude(pk=self.request.user.pk).exclude(pk__in=subscribers.values_list('user_id', flat=True)).filter(
+            Q(comment_replies__comment_id__in=relevant_comment_ids) | Q(**{('%s__pk__in' % COMMENTS_RELATION_NAME): relevant_comment_ids})
+        ).prefetch_related(
             Prefetch('comment_replies', queryset=replies),
             Prefetch(COMMENTS_RELATION_NAME, queryset=comments)
-        ).exclude(
-            Q(comment_replies__isnull=True) & Q(**{('%s__isnull' % COMMENTS_RELATION_NAME): True})
         )
 
         # Skip if no recipients
         if not (global_recipient_users or thread_users):
             return

Vulnerable code:         thread_users = get_user_model().objects.exclude(pk=self.request.user.pk).exclude(pk__in=subscribers.values_list('user_id', flat=True)).prefetch_related(
        ).exclude(
            Q(comment_replies__isnull=True) & Q(**{('%s__isnull' % COMMENTS_RELATION_NAME): True})
Fixed code:         self.never_emailed_user = self.create_user('never-emailed')
        # Add comment and reply on a different page for the never_emailed_user
        # They should never be notified
        comment_on_other_page = Comment.objects.create(
            page=self.root_page,
            user=self.never_emailed_user,
            text='a comment'
        )

        CommentReply.objects.create(
            user=self.never_emailed_user,
            comment=comment_on_other_page,
            text='a reply'
        )

    def assertNeverEmailedWrongUser(self):
        self.assertNotIn(self.never_emailed_user.email, [to for email in mail.outbox for to in email.to])

        self.assertNeverEmailedWrongUser()
        self.assertNeverEmailedWrongUser()
        self.assertNeverEmailedWrongUser()
        self.assertNeverEmailedWrongUser()
        thread_users = get_user_model().objects.exclude(pk=self.request.user.pk).exclude(pk__in=subscribers.values_list('user_id', flat=True)).filter(
            Q(comment_replies__comment_id__in=relevant_comment_ids) | Q(**{('%s__pk__in' % COMMENTS_RELATION_NAME): relevant_comment_ids})
        ).prefetch_related(