We often hear to always sanitize user input when dealing with an endpoint that might create a write to a database to avoid SQL injections or escaping HTML output that is derived from some user input to prevent XSS attacks, but I’ve never heard about any of this discussed in the context of Discord bots. The cscareers Discord experienced quite a unique incident today and would love to share the learnings with folks that run their own bots/servers.
Within the cscareers server, we have a few Discord bots. One of which is our inhouse bot that was created to deal with specific requirements to our server. We have some commands that depend on user input to return some data. In some cases, those commands may fail and we could some generic message like the following:
This bot has been around in the server since the start of our server - so around two years. We don’t actively develop on this bot anymore as a lot of the technical decisions were made to purely optimize developer velocity - which made sense when we were trying to push features out as fast as possible.
On Feburary 26, 2022 at around 4:30 PM PST, we experienced some malicious actors that took advantage of one our commands’ output. In this command, if a lookup on a given key failed, we would output a message that included the user input. Doesn’t sound so bad when we look at the above screenshot, right?
On Discord, there is a concept of when an authorized user sends @everyone in a message, it will ping (notify) everyone on the server. On an individual role level, we can define who has access to emit a ping when they send @everyone in a message. At the time of this incident, our main bot had equivalent permissions to the admin role’s permissions since we have a good amount of tooling on this bot within the admin channels - this was a mindless decision when we created the bot around two years ago.
A normal user would be able to send an @everyone as input to a command, and if the command returned the user input in the error message, it would ping @everyone - which isn’t ideal when your server contains more than 45,000 people:
Thankfully we had some mods around that were able to remedy this fire quickly by banning the bad actors and restricting messages in channels temporarily. By 5:00 PM PST, I was able to get online and create a hot-patch fix for this exploit.
There are a few fixes when handling this issue:
Solution #1: Creating more tight permissions on the bot roles and individual user bot roles on the Discord platform.
Discord permissions have the concept of user-specific permissions and role-specific permissions. Neither the bot-role permissions or the user-specific (cscareers.dev) role permissions were explicitly defined to not allow mentions. This was our first fix (and should actually be good enough in theory)
Solution #2: Properly sanitize user input that the bot commands execute (code change).
The code fix to this is quite simple. For every incoming message that the bot receives, we do some slight mutations on them and construct each part of the message into a command and args respectively. Now, we run both variables through a sanitizer function to ensure that no malicious input is passed to the downstream functions
In retrospect this bug was an extremely silly and an avoidable one - but I’m quite surprised that it took two years for it to be discovered! The only losses we experience in something like this is a few people either leaving the server or muting pings - which is understandable. I hope this goes without saying now - but go check your own permissions / sanitization methods on your Discord bots!