A Novel CSP Bypass Using `data:` URI
This post originally appeared at https://www.nccgroup.com/ae/about-us/newsroom-and-events/blogs/2019/april/a-novel-csp-bypass-using-data-uri/ but is copied here due to linkrot.
I couldn’t find an XSS payload which worked in a situation with a very restrictive CSP. This post documents the things I tried and the solution I ultimately found, with the help of some coworkers.
Standard XSS Payloads Blocked By CSP
On a recent web application assessment, I ran into a DOM-based XSS1 vulnerability
which had me stumped. It was a classic DOM-based XSS: the application was returning
user data in a JSON blob, and inserting it into the page using
innerHTML is used to modify the DOM,
<script> tags don’t execute[^w3],
but that’s not an issue normally. Many other tags can be used, such as the
following payload using an
eval() or included into a web
page (the DOM)
through a function that doesn’t perform output encoding.
<img src=x onerror='alert(1)'/>
In the application I was testing, though, every payload I tested was blocked
by the extremely restrictive Content Security Policy.
resources, such as images or CSS), blocking other sources. As a result,
a strong CSP can prevent XSS even when the application itself handles user
input unsafely, by blocking inline scripts from executing at runtime.
The CSP used by the site boiled down to the following (with
standing in for the domain I was testing):
Content-Security-Policy: default-src 'self' data: *.myclient.com; connect-src 'self' data: *.myclient.com; font-src 'self' data: *.myclient.com fonts.gstatic.com; img-src 'self' data: *.myclient.com; script-src 'self' data: *.myclient.com; style-src 'self' data: *.myclient.com; report-uri /_csp; upgrade-insecure-requests
I entered the policy into Google’s CSP Evaluator,
an extremely helpful tool which analyzes policies to detect weaknesses. The
obvious weakness, of course, is that this policy allows the
allow HTML tags to be created with inline content, rather than reaching
out to and making an additional request to the server. For example, an inline image
might look like:
Since the CSP permits HTML tags to specify content via a
data: URI, it should be
information out there about using an HTML tag with a
find relied on the
<script> tag - but we can’t use any of them, since our payload
is included in the page using
innerHTML. So the crux of the problem is this: find
data: URI, without using a
<script> tag. Additionally, it can’t load data from untrusted domains.
(If you’re the impatient type, this is where you can skip to the end of the post
to find out what ultimately worked.)
href attribute of an
a tag can be set from a data URI. However, navigation
to data URIs is disabled in
and IE/Edge for a number of years.
This payload may still work in other browsers, but would require a link click. Given
those restrictions, I decided it wasn’t the payload I was looking for.
<a href='data:text/html,<script>alert(1)</script>'>my link</a>
SVGs are commonly used to bypass XSS filtering. I thought they might work here, too, but inline scripts and event handlers I tried were unsuccessful.
<svg> <script>alert(1)</script> </svg>
<svg> <g><rect onclick="alert(1)" width="300" height="300"/></g> </svg>
<svg> <script src='data:,utf8;alert(1)'></script> </svg>
These SVG payloads failed for the same reasons my earlier payloads failed:
the CSP), or a
<script> tag which doesn’t execute with
last attempt was embedding an SVG into the
data: URI of an
<img src="data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <g> 	<rect onclick="alert(1)" width="300" height="300"/> </g> </svg>" alt="">
With SVGs exhausted, I moved onto iframes. It was pretty straightforward to get an iframe to render, and I controlled the style and content as well. It probably wouldn’t have been too hard to use this to turn the site into a phishing page. However, I was having a lot of trouble getting scripts to execute, and SOP/CSP violations when trying to navigate the frame or main window.
<iframe src="data:text/html,<html><a href=example.com>click me!</a></html>"/>
At this point, I didn’t want to spend all my time testing only a single bug,
so I wrote a short email to our internal mailing list to see if any of my
coworkers had any ideas. The first break came from a coworker who suggested
defer attributes for a script inside an iframe.
This allowed me to get scripts executing inside the frame.
However, the origin of the iframe was the null origin.
Basically, the browser treated the iframe as if it was loaded from a local file,
clearing the iframe’s
document.domain, and thus the SOP prevented communication.
the embedding page; I couldn’t modify the page, read cookies, or anything
else I wanted to do.
After sharing those results with the thread, a second coworker came along
to provide the ultimate solution. This solution uses the iframe’s
attribute. The main difference in the
srcdoc attributes, in this
case, is that
srcdoc uses the same origin as the embedding page (when used
for an un-sandboxed iframe).
A Stack Overflow answer
provides an explanation for the different attributes. Essentially, the
attribute was added along with iframe sandboxing (the
Older browsers, which do not support the
sandbox attribute, would simply ignore
it. As a result, sites which relied on this attribute would fall back to insecure
behavior if they used the
src attribute. Using
srcdoc, on the other hand,
meant that older browsers would simply render an empty iframe if sandboxing was
So here it is, an XSS payload which:
- Triggers when written with
- Doesn’t load data from external domains
I’m sure there are other payloads out there, so if you’re looking for a fun
challenge, tweet me3 your own solution! I bet other HTML tags like
<canvas> have some fun properties to play around with.
Big thanks to Jeff Dileo and Andy Grant, who came up with the deferred
script tag and
srcdoc solutions respectively - the client really appreciated
knowing whether this was something that could be exploited practically.
DOM-Based XSS occurs when user input is handled unsafely in client-side ↩