Discord proxies all its media (attachments, emojis, profile pictures) through Cloudflare with caching to minimize latency. Recent security research into Cloudflare caching has revealed a new type of caching exploit that Discord is vulnerable to.
Cloudflare Cache stores copies of frequently accessed content (such as images, videos, or webpages) in geographically distributed data centers located closer to end users than origin servers, reducing server load and improving website performance.
The emphasis here is "geographically distributed data centers". Cloudflare has the biggest cloud provider network with data centers in 330 cities across more than 120 countries. In the US East alone, they have 22 separate locations. Because of their huge network, Cloudflare PoPs (points of presence) for caching are usually less than ~200 miles from the user.
This huge network of data centers introduces a huge flaw. As mentioned earlier, Cloudflare partitions cache through data centers, and because of this bad actors can very easily correlate caches and triangulate user locations. Each of Cloudflare's data center locations has its own local cache storage to serve content faster so it's possible to check each datacenter to see where content was cached.
When you send an image attachment to a user, their Discord client downloads the attachment from https://cdn.discordapp.com/attachment/abcasd/adscdc.png
, the request first goes through Cloudflare's anycast network which leads to the closest datacenter to the user. The datacenter handling the request attempts to serve the content from its local storage if it has its cached, otherwise it sends the request to the origin, caches it, and returns the response. We can track this through the cf-cache-status
header in HTTP requests: HIT, or MISS. Since the cache is stored locally by a single datacenter, requests handled by any other data center must refetch the resource and save a separate copy to its cache.
We can very easily track a user's location this way. By sending a request to every single datacenter in Cloudflare's network, and comparing the cf-cache-status
response header, we can see what datacenter handled and cached their request and triangulate their location based on this data, as datacenters are usually ~150 miles from the user's actual location.
I created a tool called Cloudflare Teleport to facilitate this, it allows you to send HTTP requests to different Cloudflare data centers (https://github.com/hackermondev/cf-teleport) and bypass their Anycast network. Currently, my tool is one of the only ways to do this.
The first variation of this vulnerability is pretty simple. Discord custom emojis are loaded through the CDN, and cached to Cloudflare, and vulnerable to what I described above. This means with a single message with a custom emoji, an attacker can grab someone's location within a ~60-mile radius. I can see this being a very useful technique for doxxing and harassment.
There is a caveat to this though, that can also be used to extend the attack's impact. Emojis loaded through the CDN don't use the same image URL for every client and user. Emoji CDN URLs follow this format:
https://cdn.discordapp.com/emojis/[id].{webp,png,gif}?size={16,20,22,24,28,32,40,44,48,56,60,64,80,96,100,128,160,240,256,300,320,480,512,600,640,1024,1280,1536,2048,3072,4096}{&quality=lossless}
The client goes through multiple checks and finds the best image version to use for the emoji based on the client version/browser/screen dimensions:
export function getEmojiURL({id, animated, size, forcePNG = false}: EmojiOptions): string {
const staticEmojiExtension = SUPPORTS_WEBP && !forcePNG ? 'webp' : 'png';
// Don't use lossless on Android to improve memory and performance.
const qualityParam = SUPPORTS_WEBP && !IS_ANDROID ? '&quality=lossless' : '';
if (window.GLOBAL_ENV.CDN_HOST != null) {
return `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${
animated ? 'gif' : staticEmojiExtension
}?size=${getBestMediaProxySize(size * getDevicePixelRatio())}${qualityParam}`;
} else {
return (
location.protocol + window.GLOBAL_ENV.API_ENDPOINT + Endpoints.EMOJI(id, animated ? 'gif' : staticEmojiExtension)
);
}
}
This results in 124 (2 * 2 * 31) possible cdn URLs a client can use to load an emoji. We can reduce this number to 62 (2 * 31) if we use an animated emoji (so the image extension is always gif). We have to send an individual HTTP request to every Cloudflare datacenter (300+) for every variation (62) to find what datacenter handled the user's request when they loaded the emoji by looking at the cache status. While this sounds hard, it is straightforward to do especially with HTTP/2.
Because of this, we can also fingerprint the user's client and gain some basic information (window size, device type) based on the size
and quality
their client used to load the URL.
Steps:
- In DevTools, Use Network Request Blocking to block
cdn.discordapp.com/*
so your client cannot load the CDN URLs and mess up the triangulation - Create a custom animated emoji in a server you own
- Send the emoji in a message to your target
- Use the Cloudflare Teleport tool to find what datacenter the target connected to when their client loaded the emoji.
What else loads through the CDN? Profile avatars in push notifications.
Using the same technique I described above, we can perform a 0-click attack on any Discord user to grab their geolocation at any moment in time. When sending push notifications (messages, friend requests, etc), Discord usually includes the avatar of the user who triggered it in the phone notification. This avatar is loaded through the CDN and is vulnerable to the same techniques I described earlier.
From my research, a friend request is the best way to trigger a push notification. Discord always sends friend request notifications to mobile devices regardless of whether you're online on another platform or your message requests/spam filters settings.
Push Notifications also use a single, unique format:
https://cdn.discordapp.com/avatars/{id}/{hash}
so it's much faster to check caches (~6 seconds). User avatars loaded in the client use a completely different format (they have an image extension and size parameter) so you can be 100% sure you're checking who loaded your avatar through a push notification.
Steps:
- Change the user avatar to anything
- Send a friend request to the target
- Use the Cloudflare Teleport tool to find what data centers loaded your avatar. The format for push notifications is
https://cdn.discordapp.com/avatars/{id}/{hash}
. Basically, with just a Discord username, we can tap the current geolocation of anyone's phone. No user interaction is required.
An attacker is able to grab the geolocation of any Discord user (~60-mile radius) in the background without any user interaction. This can be used to track mobile devices and for doxxing campaigns. With emojis, an attacker can also fingerprint users and grab some basic client data. Keep in mind, this works with any sort of media loaded on Discord so the impact is very wide.
very interesting; thanks for sharing this!