Skip to main content

The postMessage sink for Cross-Site Scripting (XSS) vulnerabilities is less well known than the more traditional reflected and stored XSS. This makes the postMessage() method a promising area to review for vulnerabilities that others may have missed. This blog post serves as a very quick introduction to postMessage, how XSS vulnerabilities can manifest when postMessage is used incorrectly and how to test for this issue, and concludes with some links to case studies and further training material. This post is not meant to be a comprehensive analysis of this vulnerability; it is instead intended to quickly get the reader up to speed on the basics of this vulnerability and point them towards additional learning resources.

What is postMessage?

If you’re not familiar with postMessage, the MDN documentation is a good place to start: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

In short, postMessage is a mechanism that allows different web windows to communicate. If one page creates an iframe or popup and then those objects (for example, the website home page and the popup) need to be able to communicate, postMessage can be used. The setup will feature one window with some JavaScript that acts as a listener for incoming messages, and one side that issues the messages. When hunting for vulnerabilities, we’re interested in the listening side; we want to see if the messages are mishandled in any way – such as reflecting some input back to the user without doing any kind of output encoding, which leads to XSS.

What does the use of postMessage look like? How do we communicate with it?

Before we move on to potential issues with the use of postMessage, let’s quickly look at an example of this method so that we know what we’re looking for. (The example below is a lightly modified version of the code available in the MDN documentation linked above.) This example is not realistic and is designed to be as simple as possible in order to illustrate the basics of postMessage. If you’d like to see how a postMessage listener might look in the real world, be sure to examine the case studies linked to in the Case Studies and Additional Practice section at the end of this post.

In this very simple example, we’re adding a listener for a postMessage. When a postMessage is received, the (event) block is triggered. In this case, all this block does is display an alert displaying the received message.

How would a user send a message to this listener? For simplicity’s sake, we’ll use an iframe and an onload event handler. Here’s a simple page that will open the above example page in an iframe and send a message to the listener (note that it assumes that the above page is saved as postmessage.html and is hosted on a local web server):

If you want to see other ways a site could send a postMessage, there’s a variety of other ways to do this; you can see some examples on the following HackTricks page:

https://book.hacktricks.xyz/pentesting-web/postmessage-vulnerabilities#send-postmessage

In this case, we’re opening the page with the listener in an iframe. When it loads, we’ll use the postMessage method to send the message ‘Hi’ to the newly opened iframe. The listener will receive the message and then perform the processing in its (event) block. Here’s a screenshot showing the alert box from the listener being triggered, demonstrating that the message has been received and processed:

An alert box is displayed.

What can go wrong?

Now that we understand the basics of postMessage, let’s discuss some possible issues that can arise when using it. By default, a postMessage listener will listen for a message from any origin. While this isn’t inherently a vulnerability, it’s possible that the listener isn’t actually meant to accept messages from an arbitrary origin, and there’s really only one or a handful of origins that are expected to be sending messages to it. Unless origin checking is performed explicitly, though, there’s nothing to stop an unexpected origin from also sending messages to the listener.

This matters because if origin validation is performed, it’s possible that even if there’s a vulnerability in the way a message is handled, perhaps only one origin is trusted to send messages to the listener, and the vulnerability is likely unexploitable in all but the most extreme cases. On the other hand, if you find a listener that performs no origin validation, you’ve identified extra attack surface that might merit further investigation. A good takeaway for developers is to always perform origin validation when using postMessage, unless there’s a very specific reason to expose the listener to all origins.

After verifying that no origin validation is performed, you’ll want to analyze how the provided message is handled. At this point, you’re pretty much hunting for DOM XSS. There’s not much unique to postMessage beyond how the attack surface is being reached.

Let’s see a very simple example of a vulnerable postMessage handler:

This time, the listener processes the provided message. In this case, it saves the message to a variable and then calls eval() on it without doing any kind of validation on the input. Of course this is an extremely contrived vulnerability, but it’s enough to illustrate the idea.

Exploitation is very straightforward. We’ll just update the message we send to contain some JavaScript we want executed, which will be passed to eval() by the listener. For demonstration purposes, we’ll just use an alert box. Here’s what that looks like:

Here’s a screenshot showing that the message was processed, and the provided JavaScript was evaluated, leading to our alert box being triggered:

The XSS payload is processed by the listener, causing an alert box to be displayed.

So how could this be corrected? An obvious choice would be to not use eval() or any other functionality that will execute user input or output it in an unsafe manner; this is the same mitigation you’d recommend for any other web app functionality that’s vulnerable to XSS. We’ve already touched on origin validation, so let’s see it in action.

This example still contains the vulnerable message handling with eval(), which isn’t ideal; however, there’s now a check to ensure that the origin sending the message is https://trustfoundry.net. If you try using the previous exploit code on this updated listener example, you’ll see that the message is no longer processed, since the message is not coming from https://trustfoundry.net. Even though the vulnerable code technically still exists, it’s no longer exploitable unless you find a way to send messages from https://trustfoundry.net, which we certainly hope doesn’t happen.

Testing for postMessage XSS

While this post is not intended to be a comprehensive guide to identifying postMessage XSS, we’ll quickly cover the basics of testing for this issue. For this section, we’ll use the PortSwigger Academy lab available here: https://portswigger.net/web-security/dom-based/controlling-the-web-message-source/lab-dom-xss-using-web-messages

Burp Suite’s embedded browser ships with an extension that contains a tool called DOM Invader:

https://portswigger.net/burp/documentation/desktop/tools/dom-invader

This tool can be used to identify and exploit DOM-based issues by identifying sources and sinks and observing postMessages sent by the application. For our purposes, it can be useful for identifying postMessage listeners and issuing messages to them.

For working through the following example, when enabling DOM Invader, be sure to enable all the associated settings, as seen in the following screenshot:

DOM Invader settings.

Note that depending on how incoming messages are handled, having DOM Invader automatically send messages may cause unexpected behavior, so when using this extension, be prepared to change these settings if something unusual is happening.

After beginning the lab linked above, enabling DOM Invader, and enabling postMessage interception, let’s take a look at the DOM tab in DOM Invader:

DOM Invader menu.

In the screenshot, we can see that an element.innerHTML sink has been flagged for a div element with the ID “ads”. If we then take a look at the page source and search for that div, we can find both the div and a postMessage listener below it:

If we then navigate to the Messages tab in DOM Invader, we can see a list of postMessages that DOM Invader has attempted to automatically send to any listeners:

DOM Invader message list.

Clicking on one of these entries will present a page showing the message sent (either the message as it would be originally or the version containing modified data provided by DOM Invader). A description section is present that includes details like whether origin validation is present and whether unencoded characters are reflected in the sink.

Helpfully, DOM Invader also allows modifying the message data and resending the message from within the extension interface. The following screenshot illustrates making use of this functionality to resend a message with the data “TrustFoundry”, which is then reflected in the “ads” div element:

Resending a message with DOM Invader.

As we’ve seen in this section, DOM Invader can be used to identify postMessage listeners and record and replay messages sent to the listeners. DOM Invader will also provide some information about the messages sent, including whether any origin validation is present. In the interest of not giving away the full solution to the lab challenge, exploiting the issue shown in these examples is left as an exercise for the reader.

Case Studies and Additional Practice

We’ve now covered the basics of postMessage and a simple example of vulnerable postMessage handling. To help cement this knowledge, here’s a quick list of case studies involving postMessage XSS, along with some brief summaries; these will give you a chance to see real-world examples of postMessage vulnerabilities:

https://labs.detectify.com/writeups/postmessage-xss-on-a-million-sites/

  • This post covers identifying a postMessage listener that performed no origin validation beyond requiring an HTTP / HTTPS page origin. The message handling code was deobfuscated to identify that the provided message would be used to add a new script element to the page, leading to XSS.

https://hackerone.com/reports/231053

  • This post details a listener that performed no origin validation but did perform escaping of the message content in an attempt to prevent XSS. The issue reporter identified a method of bypassing the escaping, with the added wrinkle that they needed to leverage an object that could specifically be sent via postMessage.

https://hackerone.com/reports/900619

  • Finally, this post examines a listener that did perform some minimal origin checking by ensuring that the origin and referrer value matched. However, this check was possible to satisfy while sending messages from an arbitrary origin; the issue reporter identified a route that would render postMessage content as HTML, leading to XSS.

Additionally, PortSwigger Academy has three labs involving postMessage XSS. If you’d like to give them a try, they are available here:

https://portswigger.net/web-security/dom-based/controlling-the-web-message-source

This blog post provided a brief introduction to the basics of postMessage and how improper usage of postMessage can potentially lead to XSS. Hopefully, this post has helped teach you something new about this specific form of XSS.

Josiah Pierce

Josiah enjoys competing in Capture the Flag (CTF) competitions in his spare time and is interested in exploit development and reverse engineering.