Bing Bong Your Discord Bot is Pwned
Discord is one of the top communication apps in the world. With over 190 million monthly active users in 2024, it has changed the game for text-based online communication. The main place people chat on Discord is in servers, which are free to create for anyone. I’ve found the platform to be great for gaming communication through the voice and text systems it provides for my friends and me. Discord does not just focus on small communities, however; it has a focus on larger groups as well. These servers, sometimes with over 1 million users, usually specialize in one topic that server members are passionate about. This ranges from AI, for a server like Midjourney, to gaming, for a server like Marvel Rivals. Usually, in large servers like these, Discord’s built-in moderation tools don’t provide enough support to monitor millions of users even with large moderation teams. That’s where Discord bots come in.
command.reply(“Hello World”)
Discord bots allow bot owners to perform actions using the API directly. This gives bots the ability to do significantly more complex actions than users are allowed. These actions range from a game of ping pong to a moderation system with multiple levels of infractions. Overall, Discord bots significantly improve the experience for server owners and users of the platform.
Capitalism Strikes Again
While some Discord bots are only made for one or a subset of servers, there are many “commercial” Discord bots. These bots allow anyone to add them to their server and use the functionality that they provide. Almost all commercial bots also offer a paid plan which supports the cost of operating and managing the bot’s complex systems. Additionally, developers are able to subsidize free users with paid ones which creates a great system for everyone: server owners get a bot with many features, being able to pay to get more, and bot developers get capital to run the bot and continue development.
config.txt
Since commercial Discord bots are not run on server owners’ devices, they have to provide a way to configure the bot. Sometimes this is done with commands, but that can be annoying to set up (speaking from experience). So, to avoid that pain, bot developers usually set up a web portal for server owners to configure the bot from. That is the center of my “attack”. The reason I chose to target these and not another feature of the bot was twofold:
- I had been doing a lot of web hacking recently with open source software.
- There have been vulnerabilities found in the past through the web portals.
So, I thought it would be a fun exercise to see if I could find a vulnerability myself. Very Important Note: DO NOT TEST WITHOUT PERMISSION! I got verification from all bots here that no action will be taken against me for this issue. See the Footnote for official policies.1
./setup.sh2
I started with a popular Discord bot Dyno. I went through the web portal to see if I could find any area where user input was taken in and not sanitized. I mainly did this through the debugger tool in Firefox where I searched for the phrase innerHTML. I eventually found embeds were created using innerHTML since they used markdown to render the fields.
Start Your Engines
I had found issues with markdown parsing in apps before, so I tried some of those common attack methods to see if any of them would work here. None of them did, which I thought would be the end of it. It was not. I found that certain paths inside rendered markdown bypassed the HTML sanitization step. This was true for 6 paths, which I figured out were all mentions. The easiest ones to figure out were @here and @everyone, both of which would not be injectable since the content was static. For the other four methods, users, roles, emojis, and channels, I was unsure how they were rendered since the frontend code was minified.
Vibefailing
AI has had huge advances in cybersecurity over the past few months. But, at the time of finding, its scope was more limited. One thing I found it was good at before many of these advances was reading minified frontend code, explaining its function, and identifying any possible attack surface. Obviously, AI companies don’t want everyone going around and doing this to every website they see, so they prevent that functionality by default for users. Most AI companies provide exceptions to this for good-faith security research, which I was able to apply for and receive from Anthropic because of other security issues I had found in the past. I asked whether this system was vulnerable and it told me (I lost the chat :sad:) something along the lines of “This system grabs the data for all mentionable items from the Dyno API. This system is not vulnerable since the only editable data is being pulled from Discord where it is sanitized.”
Perseverance
As much as I would like to believe everything AI says (my life would be much easier), I don’t. I paid attention to the last part of the message and thought of a possible attack: if I could create an XSS payload that ran through Discord, it would end up in the embed and be executed on Dyno’s website.
Trial and Error
That was easier said than done though. Discord, unsurprisingly for a very large company, has a lot of protections built-in to prevent XSS attacks on their platform, but that also means preventing XSS attacks within Dyno since it relies on their API.
The first route of attack I had tried was through emojis. I planned to create a malicious SVG and upload it as an emoji. But, Discord doesn’t allow uploading non-raster images, and it will simply crop and convert it to a PNG via the client methods. Even if you try using the API directly, it will throw an error.
After that, I tried to use display names since those aren’t sanitized. Smartly, though, Dyno rendered the username, not the display name of the user. Since usernames cannot contain any non-Latin characters, this failed as well.
Then, I tried channels which also did not work since channels, just like usernames, can only contain Latin characters.
Finally, I tried the sixth option—roles. I originally thought that Discord would strip non-Latin characters from role names, but it seems they’ve changed that recently. So, when I made an attack payload that was just <img src=x>, it displayed a broken image on the Dyno dashboard. It worked!
Evaluation
After that, the new goal was to demonstrate impact by showing that an injected script could access the Dyno API. Because Discord limits the length of role names to 64 characters, I needed to have an extremely small payload to be in that range. Dyno also had long API URLs that would not fit in 64 characters. I decided the best method to show susceptibility would be to get a script from my server and then somehow execute that in the context of Dyno. Luckily, I own a domain name that is seven characters including the suffix, so I wasn’t going to have any issues with the website name length. The question was how to execute that script in the context of Dyno’s website. Thankfully for me and unfortunately for Dyno, this wasn’t difficult. Dyno’s Content Security Policy (CSP) headers did not prohibit fetching scripts from outside sources nor did they prohibit calls to eval. Thus, I simply made a script that had a filename within the character limit and evaled the script after fetching it.
alert(“Nuked”);
A script like mine, done by a malicious actor, had the potential to compromise thousands or potentially millions of Dyno’s servers because, crucially, Dyno’s auth isn’t split up on a server by server basis---your Dyno account is your Discord account; one account can access every server managed by the same email.
{
"userId": "1234567898",
"token": $DISCORD_TOKEN,
"email": "john@example.com"
}
A basic auth cookie for a Discord bot, is not scoped to a specific server
If a malicious actor compromises one server with social engineering, they could write a script that spreads it to every other server managed by the same server owner. If then by some other means they got adjacent server owners to click on the link for their server, the attack could spread at scale.
%%{init: {'flowchart': {'clusterPadding': 20}}}%%
flowchart TD
A["Attacker controls a server"] --> B["Injects XSS payload into Dyno dashboard"]
B --> C
subgraph P2["Phase 2 — social engineering"]
direction LR
C["Attacker invites target staff to server and makes them moderator"] --> Q["Attacker sends link to Dyno dashboard"] --> D["Staff views attacker's Dyno dashboard"]
end
D --> E
subgraph P3["Phase 3 — payload fires"]
E["Script runs in victim's browser"] --> F["Uses API to read all servers victim has moderator in"] --> Z["Enables Auto Role allowing Attacker to get admin in <b>all</b> victim-controlled servers"] --> R["Notifies Attacker"]
end
R --> G
subgraph P4["Phase 4 — propagation"]
G["Attacker uses Auto Role to gain admin on victim's servers"] --> H["Payload injected into all of victim's servers' dashboard"] --> I["Dyno or Attacker sends message to server staff"]
end
I -->|"Other server staff view infected dashboard"| E
classDef label fill:none,stroke:none,color:#888,font-size:12px
The Truth Comes Out
Unfortunately, I’m not an evil villain, so I didn’t do that attack. After finding the issue and a POC, I reported it to Dyno immediately.
Exponential Pwn
If Dyno, one of the top 5 bots on Discord, was vulnerable to an exploit like this, I assumed that other top bots would be exploitable as well; I was right.
Sapphire
Sapphire is a general-purpose moderation bot built similarly to Dyno. Sapphire is one of the newer Discord bots in the commercial space for moderation. So, I was curious if this exploit would apply here as well, and it did. After looking around on its dashboard I found that many of its pages rendered embeds. And just like Dyno, it too rendered Discord-sent fields in markdown without sanitization. I reached out to the Sapphire team who patched the XSS in less than a day.
Sapphire offers bounties for bugs like this and gave me one for reporting this issue.
Welcomer
Welcomer is an open source welcome bot for Discord servers. It sends a custom message in a channel when a user joins the server. It was similarly vulnerable to the same markdown exploit. I reported the issue to them and they fixed it in 20 minutes.
Ticket Tool
Ticket Tool is a Discord bot that allows server owners to create support tickets for their server. The ticket creation screen that it generated on the web panel was vulnerable since it had an embed and rendered role names using innerHTML. I reported it to them and they fixed it in less than a day as well.
Ticket Tool offered me credit for a premium subscription as a reward for reporting this vulnerability.
Dyno
Dyno responded quickly to my initial report, which I am thankful for. However, even though they acknowledged the report quickly, I received no response from them after. The internal ticket was still open for months. Eventually, it was closed with no response to me. This confused me since I assumed they would at least say something once it was resolved. I asked them in a follow-up ticket whether they had resolved the issue, and they said their policy was not to disclose what action, if any, was taken on reports.
If I had to guess this is based on their policy for other types of reports like ones for users and server abuse. Personally, I don’t think this is the best way to handle reports for vulnerabilities. I think that disclosing any action that is taken is helpful for the reporter and improves transparency. Nevertheless, after reaching out a couple of times for clarifications on this post, they did say that the XSS was fixed.
Feedback Form
Again, huge thank you to all the developers for fixing and responding to these issues. I do have a couple of suggestions though, if you’re a big developer or even a small one.
Improvements
- USE A CONTENT SECURITY POLICY!!!!!!!!! Seriously, the impact of this issue could’ve been avoided or minimized with a CSP rule blocking
evalor scripts inserted into the DOM. To be clear, you can still usenonces to bypass rules for scripts that need to be run a certain way. - Single-server auth tokens. I have not seen this be implemented for any Discord bot that I know of, but making it so auth tokens on the dashboard domain are only usable for a single server could significantly dampen the impact of events like this if they were to happen in the future.
Footnotes
-
Sapphire, Ticket Tool, and Welcomer all said that for good-faith security research no action will be taken against reporters as long as they report the issue quickly. Dyno had a separate response, which I got permission to include here (I was asking about disclosure + safe harbor)
↩We don’t necessarily have safe harbor provisions, but when it comes to people who happen upon vulnerabilities and then report them in the way that you did, then we do go through the process that we had handled this, which is report it to the devs, they assign it a “risk” factor, patch it within a time frame they feel is appropriate depending on risk, then after a period of time we’re fine with it being made public knowledge as long as it has been patched and confirmed patched by us/devs
-
All testing was done in the context of a single server where I was the only member. The API requests did not escape the context of this single server and no cross-server requests were injected or run for any bot mentioned here. ↩