Sandbox-iframe XSS challenge solution

This is a writeup describing the solution to a small XSS challenge I posted on Twitter in May 2024

The challenge

The challange page allows for arbitrary HTML in the search parameter xss as a Base64 encoded string. The HTML will be put inside a sandboxed iframe on the same page. The page will also add a flag to the hash portion of the URL upon visiting the site. The mission was to leak this flag in the hash and show the value in an alert box.

Step 0: the setup

Let’s summarize what’s happening here. The challenge combined three steps to allow us to access the flag (stored in the hash/fragment part of the URL) from inside the sandboxed iframe.

The challenge page did not sanitize the payload inside the base64 string. The only sanitization was done on the base64 string itself to protect it from attacks breaking out in the DOM.

The two protections in place were instead a strict CSP and the sandbox iframe attribute.

The iframe sandbox contains the allow-scripts and allow-modals values, ensuring that Javascript can execute. The lack of allow-same-origin should, however, prevent access to the top window, and the origin inside the iframe will instead be null.

The setup allows us to throw a regular <script> tag into the iframe and have it evaluated. The fact that the content is loaded through srcdoc and not the src attribute will, however, make the iframe document inherit the parent page CSP. Leaving us with scripts that passes this CSP

script-src 'self'

Step 1: Bypass CSP

A CSP feature exists that is documented but sometimes overlooked. When a CSP directive is specified with a value like, the content will only be allowed to load from any path under that subpath. And given a value like, you restrict content to that file exactly.

This rule is, however, not enforced when a valid request returns a redirect. After a redirect (given by a URL that is allowed by the CSP), the new request will only match against the base domain on any CSP directives. Given the two examples above, we can load anything from after a redirect.

The feature described above might initially look like a flaw in CSP but is, in fact, a design choice to prevent other side channel information leakage. See this part of the specification and this blog post that triggered this change

When looking to bypass CSP, this is a great tool to have in your toolbelt. Also, remember that it can be useful even if you can not get an XSS. You might be able to loosen up frame-src, form-action or any other directive using the same technique.

In the given challenge, I tried to hint at this by adding an open redirect (that will be allowed as script-src due to the value 'self' in the script-src directive) and the unnecessary dependency loaded with a full path. To escalate this to a proper XSS, all that was needed was to take advantage of other scripts hosted on the same CDN that could now be loaded using a script tag like this

<script src="/redirect?url="></script>

Even if is not a valid path, given the current CSP, after the redirect browsers will only check that the content is loaded from

I decided to go with HTMX for an execution gadget, as it’s the new and cool kid on the block. A lot of people went with the more classic <script src="/redirect?url="></script> for an Angular bypass.

The two XSS versions look like this.

<script src="/redirect?url="></script>
<img src=x hx-on:error="alert(1)">

and Angular

<script src="/redirect?url="></script>
<div ng-app ng-csp>{{$on.constructor('alert(1)')()}}</div>

Step 2: accessing parent iframe

We now have the ability to execute arbitrary Javascript, but we are still stuck inside a sandboxed iframe, giving us the origin null and no access to the parent window.

Well that “no access” is a truth with some modifications. First of we are executing on the domain about:srcdoc as the content of the iframe is given through the srcdoc attribute and this differs slightly from loading an external resource. I already mentioned that the iframe document will inherit the CSP from the parent page.

The hint I gave to some people that asked nicely was

Try to do a fetch("") call from inside the iframe and look at the request; what is happening and why?

Doing a fetch("") from inside the sandboxed iframe will make a request like this

the request will fail due to CORS errors. Think about it, we are getting a CORS error as we are making a request to the origin from the origin null so that makes sense, but how did the iframe know about the parent window URL?

Quick detour. When a browser encounters a request that lack scheme and base domain it will be treated as a relative URL. Time for more docs: tells us

A relative URL (defined in [RFC1808]) doesn’t contain any protocol or machine information. Its path generally refers to a resource on the same machine as the current document. Relative URLs may contain relative path components (“…” means one level up in the hierarchy defined by the path), and may contain fragment identifiers. Relative URLs are resolved to full URLs using a base URL. [RFC1808] defines the normative algorithm for this process.

Browsers will do two steps when deciding on a base URL Check if there is a base tag in the document and use that as the base URL Use the fallback base URL From these specifications, we can see that about: domains will use something called about base URL, this value depends on the browser implementation. All browsers have concluded that this should mean “the top documents base URL”. See this thread for a discussion on this and this discussion about the specific issue we are going to abuse here

So how do we access this baseURL? There are multiple ways to achieve this. First, just look at how browsers use the value internally when creating an anchor tag

We can see in the logs that the link with an empty href attribute will inherit the URL from the parent window. The anchor will inherit the hostname and the search portion of the URL but not the hash.

However, there is an easier way to access a document’s baseURL (or URI, as we should probably call it from now on): the baseURI node attribute:

As it happens all DOM nodes have this attribute baseURI that will return the full base URL and this value will also contain the hash. The easiest way to access this is to just use the document node like this document.baseURI.

Step 3: putting everything together

We now have all the pieces we need. A full payload would look something like this

<script src="/redirect?url="></script>
<img src=x hx-on:error="alert(document.baseURI.split('#')[1])">


This trick rarely allows you to do anything useful as the URL is often not that valuable to leak. However, the setup is not uncommon in sites that host “HTML builders,” and combining it with Frans Rosens’ “OAuth dirty dancing,” it is possible to leak some hash-based access tokens.

Thanks for all the great solutions, and sorry for the delay in writing the writeup!