CVE-2020-15278: Incorrect Authorization (CWE-863)
value: Red Discord Bot before version 3.4.1 has an unauthorized privilege escalation exploit in the Mod module. This exploit allows Discord users with a high privilege level within the guild to bypass hierarchy checks when the application is in a specific condition that is beyond that user's control. By abusing this exploit, it is possible to perform destructive actions within the guild the user has high privileges in. This exploit has been fixed in version 3.4.1. As a workaround, unloading the Mod module with unload mod or, disabling the massban command with command disable global massban can render this exploit not accessible. We still highly recommend updating to 3.4.1 to completely patch this issue.
CWE 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.
CVSS v3 base score: 7.5
Published: 2020-10-28T17:15Z
Repository: https://github.com/Cog-Creators/Red-DiscordBot
Language: Python
Files changed: docs/changelog_3_4_0.rst, redbot/cogs/mod/kickban.py
Commit date: 2020-10-27 20:46:49 +0100
Commit message: Merge pull request from GHSA-mp9m-g7qj-6vqr

* Query members for unchunked guilds in massban

* that thing that is a thing ;)
Diff: diff --git a/docs/changelog_3_4_0.rst b/docs/changelog_3_4_0.rst
index 66d38fe9..2f83b87e 100644
--- a/docs/changelog_3_4_0.rst
+++ b/docs/changelog_3_4_0.rst
@@ -7,20 +7,30 @@ Redbot 3.4.1 (2020-10-27)
 | :ghuser:`absj30`, :ghuser:`aikaterna`, :ghuser:`bobloy`, :ghuser:`chloecormier`, :ghuser:`Dav-Git`, :ghuser:`Drapersniper`, :ghuser:`fixator10`, :ghuser:`Flame442`, :ghuser:`flaree`, :ghuser:`Generaleoley`, :ghuser:`hisztendahl`, :ghuser:`jack1142`, :ghuser:`KaiGucci`, :ghuser:`Kowlin`, :ghuser:`maxbooiii`, :ghuser:`MeatyChunks`, :ghuser:`NeuroAssassin`, :ghuser:`nfitzen`, :ghuser:`palmtree5`, :ghuser:`phenom4n4n`, :ghuser:`PredaaA`, :ghuser:`Predeactor`, :ghuser:`PythonTryHard`, :ghuser:`SharkyTheKing`, :ghuser:`Stonedestroyer`, :ghuser:`thisisjvgrace`, :ghuser:`TrustyJAID`, :ghuser:`TurnrDev`, :ghuser:`Vexed01`, :ghuser:`Vuks69`, :ghuser:`xBlynd`, :ghuser:`zephyrkul`
 
 Read before updating
 --------------------
 
-1. This Red update bumps discord.py to version 1.5.1, which explicitly requests Discord intents. Red requires all Prvileged Intents to be enabled. More information can be found at :ref:`enabling-privileged-intents`.
-2. Mutes functionality has been moved from the Mod cog to a new separate cog (Mutes) featuring timed and role-based mutes. If you were using it (or want to start now), you can load the new cog with ``[p]load mutes``. You can see the full `Mutes changelog below <important-341-1>`.
-3. Information for Audio users that are using an external Lavalink instance (if you don't know what that is, you should skip this point):
+1. This release fixes a security issue in Mod cog. See `Security changelog below <important-341-2>` for more information.
+2. This Red update bumps discord.py to version 1.5.1, which explicitly requests Discord intents. Red requires all Prvileged Intents to be enabled. More information can be found at :ref:`enabling-privileged-intents`.
+3. Mutes functionality has been moved from the Mod cog to a new separate cog (Mutes) featuring timed and role-based mutes. If you were using it (or want to start now), you can load the new cog with ``[p]load mutes``. You can see the full `Mutes changelog below <important-341-1>`.
+4. Information for Audio users that are using an external Lavalink instance (if you don't know what that is, you should skip this point):
 
    We've updated our `application.yml file <https://github.com/Cog-Creators/Red-DiscordBot/blob/3.4.1/redbot/cogs/audio/data/application.yml>`_ and you should update your instance's ``application.yml`` appropriately.
    Please ensure that the WS port in Audio's settings (``[p]llset wsport``) is set to the port from the ``application.yml``.
 
 End-user changelog
 ------------------
 
+.. _important-341-2:
+
+Security
+********
+
+**NOTE:** If you can't update immediately, we recommend globally disabling the affected command until you can.
+
+- **Mod** - Fixed unauthorized privilege escalation exploit in ``[p]massban`` (also called ``[p]hackban``) command. Full security advisory `can be found on our GitHub <https://github.com/Cog-Creators/Red-DiscordBot/security/advisories/GHSA-mp9m-g7qj-6vqr>`_.
+
 Core Bot
 ********
 
 - Fixed an incorrect error being reported on ``[p]set name`` when the passed name was longer than 32 characters (:issue:`4364`, :issue:`4363`)
 - Fixed ``[p]set nickname`` erroring when the passed name was longer than 32 characters (:issue:`4364`, :issue:`4363`)
diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py
index 88b9e053..56370a47 100644
--- a/redbot/cogs/mod/kickban.py
+++ b/redbot/cogs/mod/kickban.py
@@ -1,10 +1,10 @@
 import asyncio
 import contextlib
 import logging
 from datetime import datetime, timedelta, timezone
-from typing import Optional, Tuple, Union
+from typing import Dict, List, Optional, Tuple, Union
 
 import discord
 from redbot.core import commands, i18n, checks, modlog
 from redbot.core.commands import UserInputOptional
 from redbot.core.utils import AsyncIter
@@ -438,32 +438,45 @@ class KickBanMixin(MixinMeta):
 
         if not user_ids:
             await show_results()
             return
 
+        # We need to check here, if any of the users isn't a member and if they are,
+        # we need to use our `ban_user()` method to do hierarchy checks.
+        members: Dict[int, discord.Member] = {}
+        to_query: List[int] = []
+
         for user_id in user_ids:
-            user = guild.get_member(user_id)
-            if user is not None:
-                if user_id in tempbans:
-                    # We need to check if a user is tempbanned here because otherwise they won't be processed later on.
-                    continue
+            member = guild.get_member(user_id)
+            if member is not None:
+                members[user_id] = member
+            elif not guild.chunked:
+                to_query.append(user_id)
+
+        # If guild isn't chunked, we might possibly be missing the member from cache,
+        # so we need to make sure that isn't the case by querying the user IDs for such guilds.
+        while to_query:
+            queried_members = await guild.query_members(user_ids=to_query[:100], limit=100)
+            members.update((member.id, member) for member in queried_members)
+            to_query = to_query[100:]
+
+        # Call `ban_user()` method for all users that turned out to be guild members.
+        for member in members:
+            try:
+                success, reason = await self.ban_user(
+                    user=member, ctx=ctx, days=days, reason=reason, create_modlog_case=True
+                )
+                if success:
+                    banned.append(user_id)
                 else:
-                    # Instead of replicating all that handling... gets attr from decorator
-                    try:
-                        success, reason = await self.ban_user(
-                            user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
-                        )
-                        if success:
-                            banned.append(user_id)
-                        else:
-                            errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
-                                user_id=user_id, reason=reason
-                            )
-                    except Exception as e:
-                        errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
-                            user_id=user_id, reason=e
-                        )
+                    errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
+                        user_id=user_id, reason=reason
+                    )
+            except Exception as e:
+                errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
+                    user_id=user_id, reason=e
+                )
 
         user_ids = remove_processed(user_ids)
 
         if not user_ids:
             await show_results()

Vulnerable code: 1. This Red update bumps discord.py to version 1.5.1, which explicitly requests Discord intents. Red requires all Prvileged Intents to be enabled. More information can be found at :ref:`enabling-privileged-intents`.
2. Mutes functionality has been moved from the Mod cog to a new separate cog (Mutes) featuring timed and role-based mutes. If you were using it (or want to start now), you can load the new cog with ``[p]load mutes``. You can see the full `Mutes changelog below <important-341-1>`.
3. Information for Audio users that are using an external Lavalink instance (if you don't know what that is, you should skip this point):
from typing import Optional, Tuple, Union
            user = guild.get_member(user_id)
            if user is not None:
                if user_id in tempbans:
                    # We need to check if a user is tempbanned here because otherwise they won't be processed later on.
                    continue
                    # Instead of replicating all that handling... gets attr from decorator
                    try:
                        success, reason = await self.ban_user(
                            user=user, ctx=ctx, days=days, reason=reason, create_modlog_case=True
                        )
                        if success:
                            banned.append(user_id)
                        else:
                            errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
                                user_id=user_id, reason=reason
                            )
                    except Exception as e:
                        errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
                            user_id=user_id, reason=e
                        )
Fixed code: 1. This release fixes a security issue in Mod cog. See `Security changelog below <important-341-2>` for more information.
2. This Red update bumps discord.py to version 1.5.1, which explicitly requests Discord intents. Red requires all Prvileged Intents to be enabled. More information can be found at :ref:`enabling-privileged-intents`.
3. Mutes functionality has been moved from the Mod cog to a new separate cog (Mutes) featuring timed and role-based mutes. If you were using it (or want to start now), you can load the new cog with ``[p]load mutes``. You can see the full `Mutes changelog below <important-341-1>`.
4. Information for Audio users that are using an external Lavalink instance (if you don't know what that is, you should skip this point):
.. _important-341-2:

Security
********

**NOTE:** If you can't update immediately, we recommend globally disabling the affected command until you can.

- **Mod** - Fixed unauthorized privilege escalation exploit in ``[p]massban`` (also called ``[p]hackban``) command. Full security advisory `can be found on our GitHub <https://github.com/Cog-Creators/Red-DiscordBot/security/advisories/GHSA-mp9m-g7qj-6vqr>`_.

from typing import Dict, List, Optional, Tuple, Union
        # We need to check here, if any of the users isn't a member and if they are,
        # we need to use our `ban_user()` method to do hierarchy checks.
        members: Dict[int, discord.Member] = {}
        to_query: List[int] = []

            member = guild.get_member(user_id)
            if member is not None:
                members[user_id] = member
            elif not guild.chunked:
                to_query.append(user_id)

        # If guild isn't chunked, we might possibly be missing the member from cache,
        # so we need to make sure that isn't the case by querying the user IDs for such guilds.
        while to_query:
            queried_members = await guild.query_members(user_ids=to_query[:100], limit=100)
            members.update((member.id, member) for member in queried_members)
            to_query = to_query[100:]

        # Call `ban_user()` method for all users that turned out to be guild members.
        for member in members:
            try:
                success, reason = await self.ban_user(
                    user=member, ctx=ctx, days=days, reason=reason, create_modlog_case=True
                )
                if success:
                    banned.append(user_id)
                    errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
                        user_id=user_id, reason=reason
                    )
            except Exception as e:
                errors[user_id] = _("Failed to ban user {user_id}: {reason}").format(
                    user_id=user_id, reason=e
                )