CVE-2022-21683 (Python)

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.

Weakness:
CWE-200: name: Exposure of Sensitive Information to an Unauthorized Actor
description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.

Repository: https://github.com/wagtail/wagtail

Vulnerable code:
file_path: wagtail/admin/views/pages/edit.py
snippet:     def send_commenting_notifications(self, changes):
        """
        Sends notifications about any changes to comments to anyone who is subscribed.
        """
        relevant_comment_ids = []
        relevant_comment_ids.extend(comment.pk for comment in changes['resolved_comments'])
        relevant_comment_ids.extend(comment.pk for comment, replies in changes['new_replies'])

        # Skip if no changes were made
        # Note: We don't email about edited comments so ignore those here
        if (not changes['new_comments']
                and not changes['deleted_comments']
                and not changes['resolved_comments']
                and not changes['new_replies']):
            return

        # Get global page comment subscribers
        subscribers = PageSubscription.objects.filter(page=self.page, comment_notifications=True).select_related('user')
        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(
            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
        thread_users = [(user, set(list(user.comment_replies.values_list('comment_id', flat=True)) + list(getattr(user, COMMENTS_RELATION_NAME).values_list('pk', flat=True)))) for user in thread_users]
        mailed_users = set()

        for current_user, current_threads in thread_users:
            # We are trying to avoid calling send_notification for each user for performance reasons
            # so group the users receiving the same thread notifications together here
            if current_user in mailed_users:
                continue
            users = [current_user]
            mailed_users.add(current_user)
            for user, threads in thread_users:
                if user not in mailed_users and threads == current_threads:
                    users.append(user)
                    mailed_users.add(user)
            send_notification(users, 'updated_comments', {
                'page': self.page,
                'editor': self.request.user,
                'new_comments': [comment for comment in changes['new_comments'] if comment.pk in threads],
                'resolved_comments': [comment for comment in changes['resolved_comments'] if comment.pk in threads],
                'deleted_comments': [],
                'replied_comments': [
                    {
                        'comment': comment,
                        'replies': replies,
                    }
                    for comment, replies in changes['new_replies']
                    if comment.pk in threads
                ]
            })

        return send_notification(global_recipient_users, 'updated_comments', {
            'page': self.page,
            'editor': self.request.user,
            'new_comments': changes['new_comments'],
            'resolved_comments': changes['resolved_comments'],
            'deleted_comments': changes['deleted_comments'],
            'replied_comments': [
                {
                    'comment': comment,
                    'replies': replies,
                }
                for comment, replies in changes['new_replies']
            ]
        })

Fixed code:
file_path: wagtail/admin/views/pages/edit.py
snippet:     def send_commenting_notifications(self, changes):
        """
        Sends notifications about any changes to comments to anyone who is subscribed.
        """
        relevant_comment_ids = []
        relevant_comment_ids.extend(comment.pk for comment in changes['resolved_comments'])
        relevant_comment_ids.extend(comment.pk for comment, replies in changes['new_replies'])

        # Skip if no changes were made
        # Note: We don't email about edited comments so ignore those here
        if (not changes['new_comments']
                and not changes['deleted_comments']
                and not changes['resolved_comments']
                and not changes['new_replies']):
            return

        # Get global page comment subscribers
        subscribers = PageSubscription.objects.filter(page=self.page, comment_notifications=True).select_related('user')
        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)).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)
        )

        # Skip if no recipients
        if not (global_recipient_users or thread_users):
            return
        thread_users = [(user, set(list(user.comment_replies.values_list('comment_id', flat=True)) + list(getattr(user, COMMENTS_RELATION_NAME).values_list('pk', flat=True)))) for user in thread_users]
        mailed_users = set()

        for current_user, current_threads in thread_users:
            # We are trying to avoid calling send_notification for each user for performance reasons
            # so group the users receiving the same thread notifications together here
            if current_user in mailed_users:
                continue
            users = [current_user]
            mailed_users.add(current_user)
            for user, threads in thread_users:
                if user not in mailed_users and threads == current_threads:
                    users.append(user)
                    mailed_users.add(user)
            send_notification(users, 'updated_comments', {
                'page': self.page,
                'editor': self.request.user,
                'new_comments': [comment for comment in changes['new_comments'] if comment.pk in threads],
                'resolved_comments': [comment for comment in changes['resolved_comments'] if comment.pk in threads],
                'deleted_comments': [],
                'replied_comments': [
                    {
                        'comment': comment,
                        'replies': replies,
                    }
                    for comment, replies in changes['new_replies']
                    if comment.pk in threads
                ]
            })

        return send_notification(global_recipient_users, 'updated_comments', {
            'page': self.page,
            'editor': self.request.user,
            'new_comments': changes['new_comments'],
            'resolved_comments': changes['resolved_comments'],
            'deleted_comments': changes['deleted_comments'],
            'replied_comments': [
                {
                    'comment': comment,
                    'replies': replies,
                }
                for comment, replies in changes['new_replies']
            ]
        })