Today, I came across a Tweet↗ from @theo↗:
If data is accessible via a “public URL”, but that URL contains an identifier that’s more unique than UUID, is it really public?
One reader did not agree with that statement, and a good old Twitter beef started.
Let me start by agreeing with Theo: UUIDs are unique and practically unguessable through brute force. However, in my humble opinion, blindly trusting this mindset can result in security vulnerabilities. As someone who finds and reports such vulnerabilities, I need to make a case for why this mindset is bad.
The Setup #
To demonstrate my point, let’s assume we have a simple document management system in the form of a web application. Here are a few endpoints such an application could have:
GET /documents
, which retrieves a list of all documentsGET /documents/<id>
for retrieving a specific documentPOST /documents/upload
for uploading a document
If possible, always put sensitive IDs into a POST request body rather than into a URL or a query parameter. This stops referer leaks and usually, bodies do not get logged onto disk by reverse proxies and other systems in between.
The classic way of (not) doing this #
Until UUIDs gained popularity, developers would often use incrementing, simple, and numeric IDs when creating an asset. The first document might have had document ID 1000, the second one 1001, …
Those easy-to-guess IDs are by themselves not directly a security vulnerability, merely bad design. If you do authorization checks correctly in the back end, users cannot access each other’s documents simply by knowing an ID.
As a bad example, let’s assume the following flow:
- I do not have any documents saved, so I am uploading three documents.
- They get assigned ID 3001-3003.
- My browser requests
GET /documents
and I see links like:
<a class="doc-link" href="/documents/3001">Document 1</a>
<a class="doc-link" href="/documents/3002">Document 2</a>
<a class="doc-link" href="/documents/3003">Document 3</a>
- I click one of the links, and my browser makes the request
GET /documents/3001
. - The back end validates that my session token is still valid.
- The back end fetches document
3001
and presents it to me.
So far, so normal. But what if I modify the request to be GET /documents/2999
?
If the back end returns the document 2999
, this is a clear security vulnerability known as an Insecure Direct Object References↗ (IDOR). I should not be able to access other users’ assets just by knowing their ID. And that is exactly where, in theory, UUIDs come into play.
The UUID way #
Now, if we do the same flow, but the back end assigns UUIDs, my links might look like this:
<a class="doc-link" href="/documents/f4ba1dbd-dc5d-4ea8-8693-0f6c81b67248">Document 1</a>
<a class="doc-link" href="/documents/6ecec59e-81f1-45e6-a9ee-e52eb4e54964">Document 2</a>
<a class="doc-link" href="/documents/54ed780c-52cf-49c8-9284-a642dc2db99b">Document 3</a>
- I click one of the links, and my browser makes the request
GET /documents/f4ba1dbd-dc5d-4ea8-8693-0f6c81b67248
. - The back end validates that my session token is still valid.
- The back end fetches document
f4ba1dbd-dc5d-4ea8-8693-0f6c81b67248
and presents it to me.
As Theo points out correctly, it is now not feasible for another user, the attacker, to brute-force/guess those UUIDs.
The But #
Various web vulnerabilities exist that, given the right setup, would potentially enable an attacker to leak your UUIDs.
HTTP request smuggling #
Using “HTTP request smuggling”, it might be possible to leak other users’ requests↗. This depends on various architecture decisions like web server, proxies, and lastly, request structure on how UUIDs are fetched. For a good write-up, check out portswigger.net↗.
XSSi #
Cross Site Script Inclusion (XSSi)↗ is, depending on the setup, an even better/easier way of leaking UUIDs. If your web framework generates dynamic JavaScript files and the UUIDs to your document are in such JS files, your website might be vulnerable to XSSi. A great write-up on this vulnerability can be found on sidechannel.blog↗.
XSS #
Using Cross Site Scripting↗, often called XSS, an attacker can leak the UUIDs back to them in various ways by injecting JavaScript into a page that is then visited by the user. Although web frameworks like Angular and libraries like DOMPurify↗ make it harder year by year, XSS vulnerabilities are still quite relevant. They might even gain more popularity again as some LLMs produce literal garbage security vulnerabilities when you are just vibing and have no clue what you are doing.
This is not the best example for an attack vector. If the XSS payload window is big enough to allow for UUID exfiltration, it is probably big enough to ride the session of the user. (aka. “do anything”)
Since an attacker has control of the session in the user’s browser, no ID protects against leaking the documents here. The attacker can download the documents into the DOM and exfiltrate the complete documents rather than just the IDs. How documents and IDs can be leaked to the attacker depends on the Content Security Policy (CSP)↗ in place.
The Result #
If you trust the mindset “UUIDs are not guessable and thus secure”, an attacker who leaked UUIDs of your users using any of the above methods might simply download documents that he should not have access to.
On every single request, validate that the user requesting an asset has the correct permissions/ownership of the asset (Authorization check)
I hope my point is somewhat understandable. If you do not agree or have something to add, feel free to reach out via the social media links from the header or leave a comment via BlueSky.
Thanks to floyd@chaos.social↗ for input on XSSi and highlighting that XSS is not the best example for leaking UUIDs. Critique is always welcome!
Cheers