Bypassing DOMPurify for Successful XSS Execution: namespace confusion

Bypassing DOMPurify for Successful XSS Execution: namespace confusion
Seoul, Korea of December 2023

Introduce

In the past, I often recommended the DOMPurify library as a robust solution for preventing XSS vulnerabilities, particularly when a web service needs to render HTML code input by users. Its effectiveness was such that I believed bypassing it to execute an XSS attack was virtually impossible, especially when DOMPurify was integrated into functions handling user-provided HTML.

What Is DOMPurify, And Why Does It Use? 🤔

Using innerHTML can be quite handy and can significantly reduce development time. This is particularly true when displaying HTML content created by web editors like CKEditor on the front end. However, this approach introduces a notable security risk: XSS vulnerabilities.

While innerHTML (or dangerouslySetInnerHTML in React) is notoriously targeted for XSS attacks, integrating DOMPurify can serve as a robust defense. It works by stripping out malicious HTML, making it challenging to execute harmful JavaScript.

Let's look the code below.

The example usage ofDOMPurify.sanitize()

The DOMPurify.sanitize() function is designed to remove potentially harmful JavaScript code, such as alert() above. To understand the robustness of DOMPurify, you can try out a demo here. This demonstration will show you the strength of DOMPurify's sanitization capabilities.

In my experience, encountering DOMPurify in the code I'm analyzing often led me to give up early, given its efficacy in thwarting XSS attacks. 😅

GitHub - cure53/DOMPurify: DOMPurify - a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. DOMPurify works with a secure default, but offers a lot of configurability and hooks. Demo:
DOMPurify - a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. DOMPurify works with a secure default, but offers a lot of configurability and hooks. Demo: - GitHub - cure...

The Example of Misusing DOMPurify

I discovered a rare instance where hackers can bypass DOMPurify, thanks to an insight I gained while browsing Twitter. Special thanks to Kévin for this revelation.

Below is the code used in Kévin's challenge:

<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>

<svg id="svg">[USER INPUT]</svg>

<div id="body"></div>

<script>
const params = new URLSearchParams(location.search);
svg.innerHTML = DOMPurify.sanitize(params.get("html"));

// Mobile challengers <3
body.innerText = document.body.innerHTML;
</script>

## Hall of Fame
- @SecurityMB
- @ixSly
- @maple3142
- @taramtrampam
- @shafigullin
- @lbrnli1234
- @Benjamin_Aster
- @ini_apaan7

First Hint for Solving the Challenge 🔫

It's crucial to understand that the user's input is placed within an <svg> tag. While both HTML and SVG are XML-based, they differ significantly in the context of 'namespace'.

Due to their distinct namespaces, they require different parsing mechanisms. These mechanisms are not identical, which plays a key role in this challenge. Let's delve deeper into this concept through some code examples.

HTML

In HTML, according to its specifications, the inner text of a <style> tag must be treated as TEXT.

SVG

But what about SVG? In the case of SVG, the inner text of a <style> element is treated as a TAG, not TEXT. It's the first key of solving this challenge.

You can understand details below 👇

Write-up of DOMPurify 2.0.0 bypass using mutation XSS - research.securitum.com
Yesterday, a new version of DOMPurify (very popular XSS sanitization library) was released, that fixed a bypass reported by us. In this post I’ll show how exactly the bypass looked like preceded by general information about DOMPurify and how it works. If you are aware of how purifiers work and what…

Second Hint

Different namespaces exist for HTML, SVG, and other formats. This raises a question: Which namespace does DOMPurify default to? By default, DOMPurify operates within the HTML namespace, but this can be changed. Observe the following code:

// change the default namespace from HTML to something different
const clean = DOMPurify.sanitize(dirty, {NAMESPACE: 'http://www.w3.org/2000/svg'});

From this, we can infer that DOMPurify's XSS prevention is geared towards HTML, not SVG. Our exploitation strategy will leverage the differences between HTML and SVG.

Exploit 🔐

Gadget 1

The text in <style> tag will be out in raw, because <style> tag's is treated as TEXT. So DOMPurify will not work for text.

There must be one or more character in front of <style> tag.

Gadget 2

The attributes of HTML tag will be treated as TEXT.

Solution

The official solution provided by Kévin is as follows.

https://challenges.mizu.re/xss_02.html?html=a<style><!--</style><a id="--!><img src=x onerror=alert()>">
Namespaces crash course - SVG: Scalable Vector Graphics | MDN
As an XML dialect, SVG is namespaced. It is important to understand the concept of namespaces and how they are used if you plan to author SVG content. Namespaces are essential to user agents that support multiple XML dialects; browsers must be very strict. Taking the time to understand namespaces no…
Mutation XSS via namespace confusion - DOMPurify < 2.0.17 bypass - research.securitum.com
In this blogpost I’ll explain my recent bypass in DOMPurify – the popular HTML sanitizer library. In a nutshell, DOMPurify’s job is to take an untrusted HTML snippet, supposedly coming from an end-user, and remove all elements and attributes that can lead to Cross-Site Scripting (XSS). This is the b…