secrets
XS leak through cross-origin redirects — intended and unintended
Last updated
XS leak through cross-origin redirects — intended and unintended
Last updated
A secure and secret note storage system is a platform or application designed to keep your confidential notes safe from unauthorized access.
The challenge revolved around searching contents of secret notes.
Let's examine the behaviour of the search feature.
When searching for a note through /search?query=<query>
, there are two possible responses:
The note was found.
In this case, a 301 redirect is issued to http://results.wtl.pw/results?ids=<note UUIDs>&query=<query>
.
It is important to note that this is a redirect to a different subdomain. Searching on secrets
.wtl.pw
redirects to results
.wtl.pw
.
The note was not found.
In this case, a 301 redirect is issued to http://secrets.wtl.pw/#<query>
.
One thing that might be immediately noticeable is that if the note was found, then the resulting URL length is extended considerably by the ids
parameter.
A well-known technique in these kinds of scenarios is hitting the server's maximum URL limit, and detecting error status codes. However, these rely on SameSite=None
cookies for the error event detection.
The challenge had SameSite=Lax
cookies, so the primitive for any XS-Leak attack is a top-level navigation (e.g. through window.open
). There is no way to detect server response codes in a cross-origin window reference, so I started looking for other ways to detect the URL inflation.
We might not be able to detect a server-side URL length error, but can we somehow detect a client-side one? According to Chromium documentation, Chrome's maximum URL length is 2MB.
In general, the web platform does not have limits on the length of URLs (although 2^31 is a common limit). Chrome limits URLs to a maximum length of 2MB for practical reasons and to avoid causing denial-of-service problems in inter-process communication.
This is where it gets interesting! Because this is a client-side constraint, and URL fragments persist on redirects, we can open /search?query=<query>#AAA...[2MB]...AAA
to hit the length limit.
So, what happens when the URL limit is exceeded?
Apparently, it shows an about:blank#blocked
page.
As you might expect, trying to access the origin
(or any other sensitive information) of a cross-origin window reference would raise an exception.
However, when opening a page that errors out due to the 2MB constraint, the window's origin
remains that of the parent.
As an experiment, let's try a successful query.
The length of the opened URL
is exactly 2MB - 1, so the initial search URL is just under the length limit.
When the window is redirected to
the URL is extended and the length limit is hit. The window becomes an about:blank
page and its origin
remains that of the parent.
Now, if we try the same thing on an unsuccessful query, the final redirected URL falls short of the 2MB limit and the window's origin
is no longer accessible.
This can be extended to the following PoC, which brute-forces a character of the flag.
Because this PoC only tells us what is definitely not the flag (by detecting the w.origin
errors), we can implement a backend server to quickly find what is the flag by eliminating the unsuccessful queries from the charset.
The downside of this method is that the long URLs can cause significant lag on the server's admin bot. This may or may not have made the bot extremely unstable for a period of time... oops!
It turns out that there is a much faster and less laggy way of detecting the redirects. Because the redirect is to a different origin, we can use CSP violations as an oracle.
Because the query was successful, the window attempted to load http://results.wtl.pw
. But since our CSP dictates that forms can only be submitted to http://secrets.wtl.pw
, the request was blocked. We can detect this through the securitypolicyviolation
event listener.