mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
Output sanitisation (#1942)
* Add output sanitization defaults to context.send Add some common regex filters in redbot.core.utils.common_filters Add a wrapper for ease of use in bot.send_filtered Sanitize ModLog Case's user field (other's considered trusted as moderator input) Sanitize Usernames/Nicks in userinfo command. Santize Usernames in closing of tunnels. * Add documentation
This commit is contained in:
parent
6ebfdef025
commit
77944e195a
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -25,6 +25,7 @@ redbot/core/utils/data_converter.py @mikeshardmind
|
|||||||
redbot/core/utils/antispam.py @mikeshardmind
|
redbot/core/utils/antispam.py @mikeshardmind
|
||||||
redbot/core/utils/tunnel.py @mikeshardmind
|
redbot/core/utils/tunnel.py @mikeshardmind
|
||||||
redbot/core/utils/caching.py @mikeshardmind
|
redbot/core/utils/caching.py @mikeshardmind
|
||||||
|
redbot/core/utils/common_filters.py @mikeshardmind
|
||||||
|
|
||||||
# Cogs
|
# Cogs
|
||||||
redbot/cogs/admin/* @tekulvw
|
redbot/cogs/admin/* @tekulvw
|
||||||
|
|||||||
@ -44,4 +44,10 @@ Tunnel
|
|||||||
======
|
======
|
||||||
|
|
||||||
.. automodule:: redbot.core.utils.tunnel
|
.. automodule:: redbot.core.utils.tunnel
|
||||||
:members: Tunnel
|
:members: Tunnel
|
||||||
|
|
||||||
|
Common Filters
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. automodule:: redbot.core.utils.common_filters
|
||||||
|
:members:
|
||||||
|
|||||||
@ -12,6 +12,8 @@ from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_ha
|
|||||||
from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason
|
from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason
|
||||||
from .log import log
|
from .log import log
|
||||||
|
|
||||||
|
from redbot.core.utils.common_filters import filter_invites
|
||||||
|
|
||||||
_ = Translator("Mod", __file__)
|
_ = Translator("Mod", __file__)
|
||||||
|
|
||||||
|
|
||||||
@ -1321,9 +1323,11 @@ class Mod:
|
|||||||
if roles is not None:
|
if roles is not None:
|
||||||
data.add_field(name=_("Roles"), value=roles, inline=False)
|
data.add_field(name=_("Roles"), value=roles, inline=False)
|
||||||
if names:
|
if names:
|
||||||
data.add_field(name=_("Previous Names"), value=", ".join(names), inline=False)
|
val = filter_invites(", ".join(names))
|
||||||
|
data.add_field(name=_("Previous Names"), value=val, inline=False)
|
||||||
if nicks:
|
if nicks:
|
||||||
data.add_field(name=_("Previous Nicknames"), value=", ".join(nicks), inline=False)
|
val = filter_invites(", ".join(nicks))
|
||||||
|
data.add_field(name=_("Previous Nicknames"), value=val, inline=False)
|
||||||
if voice_state and voice_state.channel:
|
if voice_state and voice_state.channel:
|
||||||
data.add_field(
|
data.add_field(
|
||||||
name=_("Current voice channel"),
|
name=_("Current voice channel"),
|
||||||
@ -1334,6 +1338,7 @@ class Mod:
|
|||||||
|
|
||||||
name = str(user)
|
name = str(user)
|
||||||
name = " ~ ".join((name, user.nick)) if user.nick else name
|
name = " ~ ".join((name, user.nick)) if user.nick else name
|
||||||
|
name = filter_invites(name)
|
||||||
|
|
||||||
if user.avatar:
|
if user.avatar:
|
||||||
avatar = user.avatar_url
|
avatar = user.avatar_url
|
||||||
|
|||||||
@ -22,6 +22,7 @@ from . import Config, i18n, commands
|
|||||||
from .rpc import RPCMixin
|
from .rpc import RPCMixin
|
||||||
from .help_formatter import Help, help as help_
|
from .help_formatter import Help, help as help_
|
||||||
from .sentry import SentryManager
|
from .sentry import SentryManager
|
||||||
|
from .utils import common_filters
|
||||||
|
|
||||||
|
|
||||||
def _is_submodule(parent, child):
|
def _is_submodule(parent, child):
|
||||||
@ -292,6 +293,39 @@ class RedBase(BotBase, RPCMixin):
|
|||||||
if pkg_name.startswith("redbot.cogs."):
|
if pkg_name.startswith("redbot.cogs."):
|
||||||
del sys.modules["redbot.cogs"].__dict__[name]
|
del sys.modules["redbot.cogs"].__dict__[name]
|
||||||
|
|
||||||
|
async def send_filtered(
|
||||||
|
destination: discord.abc.Messageable,
|
||||||
|
filter_mass_mentions=True,
|
||||||
|
filter_invite_links=True,
|
||||||
|
filter_all_links=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This is a convienience wrapper around
|
||||||
|
|
||||||
|
discord.abc.Messageable.send
|
||||||
|
|
||||||
|
It takes the destination you'd like to send to, which filters to apply
|
||||||
|
(defaults on mass mentions, and invite links) and any other parameters
|
||||||
|
normally accepted by destination.send
|
||||||
|
|
||||||
|
This should realistically only be used for responding using user provided
|
||||||
|
input. (unfortunately, including usernames)
|
||||||
|
Manually crafted messages which dont take any user input have no need of this
|
||||||
|
"""
|
||||||
|
|
||||||
|
content = kwargs.pop("content", None)
|
||||||
|
|
||||||
|
if content:
|
||||||
|
if filter_mass_mentions:
|
||||||
|
content = common_filters.filter_mass_mentions(content)
|
||||||
|
if filter_invite_links:
|
||||||
|
content = common_filters.filter_invites(content)
|
||||||
|
if filter_all_links:
|
||||||
|
content = common_filters.filter_urls(content)
|
||||||
|
|
||||||
|
await destination.send(content=content, **kwargs)
|
||||||
|
|
||||||
def add_cog(self, cog):
|
def add_cog(self, cog):
|
||||||
for attr in dir(cog):
|
for attr in dir(cog):
|
||||||
_attr = getattr(cog, attr)
|
_attr = getattr(cog, attr)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import discord
|
|||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
from redbot.core.utils.chat_formatting import box
|
from redbot.core.utils.chat_formatting import box
|
||||||
|
from redbot.core.utils import common_filters
|
||||||
|
|
||||||
TICK = "\N{WHITE HEAVY CHECK MARK}"
|
TICK = "\N{WHITE HEAVY CHECK MARK}"
|
||||||
|
|
||||||
@ -20,6 +20,42 @@ class Context(commands.Context):
|
|||||||
This class inherits from `discord.ext.commands.Context`.
|
This class inherits from `discord.ext.commands.Context`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
async def send(self, content=None, **kwargs):
|
||||||
|
"""Sends a message to the destination with the content given.
|
||||||
|
|
||||||
|
This acts the same as `discord.ext.commands.Context.send`, with
|
||||||
|
one added keyword argument as detailed below in *Other Parameters*.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
content : str
|
||||||
|
The content of the message to send.
|
||||||
|
|
||||||
|
Other Parameters
|
||||||
|
----------------
|
||||||
|
filter : Callable[`str`] -> `str`
|
||||||
|
A function which is used to sanitize the ``content`` before
|
||||||
|
it is sent. Defaults to
|
||||||
|
:func:`~redbot.core.utils.common_filters.filter_mass_mentions`.
|
||||||
|
This must take a single `str` as an argument, and return
|
||||||
|
the sanitized `str`.
|
||||||
|
\*\*kwargs
|
||||||
|
See `discord.ext.commands.Context.send`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
discord.Message
|
||||||
|
The message that was sent.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_filter = kwargs.pop("filter", common_filters.filter_mass_mentions)
|
||||||
|
|
||||||
|
if _filter and content:
|
||||||
|
content = _filter(str(content))
|
||||||
|
|
||||||
|
return await super().send(content=content, **kwargs)
|
||||||
|
|
||||||
async def send_help(self) -> List[discord.Message]:
|
async def send_help(self) -> List[discord.Message]:
|
||||||
"""Send the command help message.
|
"""Send the command help message.
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import discord
|
|||||||
from redbot.core import Config
|
from redbot.core import Config
|
||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
from .utils.common_filters import filter_invites, filter_mass_mentions, filter_urls
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Case",
|
"Case",
|
||||||
"CaseType",
|
"CaseType",
|
||||||
@ -141,7 +143,9 @@ class Case:
|
|||||||
datetime.fromtimestamp(self.modified_at).strftime("%Y-%m-%d %H:%M:%S")
|
datetime.fromtimestamp(self.modified_at).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
)
|
)
|
||||||
|
|
||||||
user = "{}#{} ({})\n".format(self.user.name, self.user.discriminator, self.user.id)
|
user = filter_invites(
|
||||||
|
"{}#{} ({})\n".format(self.user.name, self.user.discriminator, self.user.id)
|
||||||
|
) # Invites get rendered even in embeds.
|
||||||
if embed:
|
if embed:
|
||||||
emb = discord.Embed(title=title, description=reason)
|
emb = discord.Embed(title=title, description=reason)
|
||||||
|
|
||||||
@ -160,6 +164,7 @@ class Case:
|
|||||||
emb.timestamp = datetime.fromtimestamp(self.created_at)
|
emb.timestamp = datetime.fromtimestamp(self.created_at)
|
||||||
return emb
|
return emb
|
||||||
else:
|
else:
|
||||||
|
user = filter_mass_mentions(filter_urls(user)) # Further sanitization outside embeds
|
||||||
case_text = ""
|
case_text = ""
|
||||||
case_text += "{}\n".format(title)
|
case_text += "{}\n".format(title)
|
||||||
case_text += "**User:** {}\n".format(user)
|
case_text += "**User:** {}\n".format(user)
|
||||||
|
|||||||
81
redbot/core/utils/common_filters.py
Normal file
81
redbot/core/utils/common_filters.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"URL_RE",
|
||||||
|
"INVITE_URL_RE",
|
||||||
|
"MASS_MENTION_RE",
|
||||||
|
"filter_urls",
|
||||||
|
"filter_invites",
|
||||||
|
"filter_mass_mentions",
|
||||||
|
]
|
||||||
|
|
||||||
|
# regexes
|
||||||
|
URL_RE = re.compile(r"(https?|s?ftp)://(\S+)", re.I)
|
||||||
|
|
||||||
|
INVITE_URL_RE = re.compile(r"(discord.gg|discordapp.com/invite|discord.me)(\S+)", re.I)
|
||||||
|
|
||||||
|
MASS_MENTION_RE = re.compile(r"(@)(?=everyone|here)") # This only matches the @ for sanitizing
|
||||||
|
|
||||||
|
|
||||||
|
# convenience wrappers
|
||||||
|
def filter_urls(to_filter: str) -> str:
|
||||||
|
"""Get a string with URLs sanitized.
|
||||||
|
|
||||||
|
This will match any URLs starting with these protocols:
|
||||||
|
|
||||||
|
- ``http://``
|
||||||
|
- ``https://``
|
||||||
|
- ``ftp://``
|
||||||
|
- ``sftp://``
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
to_filter : str
|
||||||
|
The string to filter.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The sanitized string.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return URL_RE.sub("[SANITIZED URL]", to_filter)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_invites(to_filter: str) -> str:
|
||||||
|
"""Get a string with discord invites sanitized.
|
||||||
|
|
||||||
|
Will match any discord.gg, discordapp.com/invite, or discord.me
|
||||||
|
invite URL.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
to_filter : str
|
||||||
|
The string to filter.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The sanitized string.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return INVITE_URL_RE.sub("[SANITIZED INVITE]", to_filter)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_mass_mentions(to_filter: str) -> str:
|
||||||
|
"""Get a string with mass mentions sanitized.
|
||||||
|
|
||||||
|
Will match any *here* and/or *everyone* mentions.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
to_filter : str
|
||||||
|
The string to filter.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The sanitized string.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return MASS_MENTION_RE.sub("@\u200b", to_filter)
|
||||||
@ -5,6 +5,7 @@ import io
|
|||||||
import sys
|
import sys
|
||||||
import weakref
|
import weakref
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from .common_filters import filter_mass_mentions
|
||||||
|
|
||||||
_instances = weakref.WeakValueDictionary({})
|
_instances = weakref.WeakValueDictionary({})
|
||||||
|
|
||||||
@ -70,10 +71,10 @@ class Tunnel(metaclass=TunnelMeta):
|
|||||||
self.recipient = recipient
|
self.recipient = recipient
|
||||||
self.last_interaction = datetime.utcnow()
|
self.last_interaction = datetime.utcnow()
|
||||||
|
|
||||||
async def react_close(self, *, uid: int, message: str):
|
async def react_close(self, *, uid: int, message: str = ""):
|
||||||
send_to = self.origin if uid == self.sender.id else self.sender
|
send_to = self.origin if uid == self.sender.id else self.sender
|
||||||
closer = next(filter(lambda x: x.id == uid, (self.sender, self.recipient)), None)
|
closer = next(filter(lambda x: x.id == uid, (self.sender, self.recipient)), None)
|
||||||
await send_to.send(message.format(closer=closer))
|
await send_to.send(filter_mass_mentions(message.format(closer=closer)))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def members(self):
|
def members(self):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user