PostMessage Vulnerabilities. Part I
This is the first part of a two series of articles about postMessage vulnerabilities. This part is an introduction to what is a postMessage, basic exploitation, detection and mitigation. The second part is an analysis of real cases reported in Bug Bounty scenarios.
What is a postMessage?
According to Mozilla:
The window.postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it.
Let’s see an example.
Supose we have a main website (1.html) that communicates with another website (2.html). In the second website there is a back button that changes when the navigation in the first website changes. For example, in website1 we navigate to “changed.html”, then the back button in website2 points to “changed.html”. To do that, postMessage is used and sends the value of website1 to website2.
The code in 1.html is the following:
<!DOCTYPE html>
<html>
<head>
<title>Website 1</title>
<meta charset="utf-8" />
<script>
var child;
function openChild() {child = window.open('2.html', 'popup', 'height=300px, width=500px');
}
function sendMessage(){
let msg={url : "changed.html"};
// In production, DO NOT use '*', use toe target domain
child.postMessage(msg,'*')// child is the targetWindow
child.focus();
}</script>
</head>
<body>
<form>
<fieldset>
<input type='button' id='btnopen' value='Open child' onclick='openChild();' />
<input type='button' id='btnSendMsg' value='Send Message' onclick='sendMessage();' />
</fieldset>
</form>
</body>
</html>
There are two buttons:
- The first one opens a popup containing 2.html through
openChild()
function - The second one sends a message through
sendMessage()
function. To do that a message is set definingmsg
variable and then callingpostMessage(msg,'*')
.
The code in 2.html is the following:
<!DOCTYPE html>
<html>
<head>
<title>Website 2</title>
<meta charset="utf-8" />
<script>
// Allow window to listen for a postMessage
window.addEventListener("message", (event)=>{
// Normally you would check event.origin
// To verify the targetOrigin matches
// this window's domain
document.getElementById("redirection").href=`${event.data.url}`;
// event.data contains the message sent
});function closeMe() {
try {window.close();
} catch (e) { console.log(e) }
try {self.close();
} catch (e) { console.log(e) }}
</script>
</head>
<body>
<form>
<h1>Recipient of postMessage</h1>
<fieldset>
<a type='text' id='redirection' href=''>Go back</a>
<input type='button' id='btnCloseMe' value='Close me' onclick='closeMe();' />
</fieldset>
</form>
</body>
</html>
There is a button and a link:
- The link handles the back redirection. The
href
field changes according the data received with the listenerwindow.addEventListener("message", (event)
. After receiving the message, the data in the event is read fromevent.data.url
and passed tohref
. - The button closes the window calling the function
closeMe()
A basic vulnerability
PostMessages if are not implemented correctly could lead to information disclosure or cross-site scripting vulnerabilities (XSS).
In this case, 2.html is expecting a message without validating the origin, therefore we could host a webpage 3.html that will load 2.html as an iframe and the invoke the postMessage()
function to manipulate the href
value.
<!DOCTYPE html>
<html>
<head>
<title>XSS PoC</title>
<meta charset="utf-8" />
</head>
<body>
<iframe id="frame" src="2.html" ></iframe>
<script>
let msg={url : "javascript:prompt(1)"};
var iFrame = document.getElementById("frame")
iFrame.contentWindow.postMessage(msg, '*');
</script>
</body>
</html>
In this example, the malicious msg
variable contains the data {url : "javascript:prompt(1)"};
, that will be send to 2.html. 2.html after processing, will change the value in the <a href
to the value of the msg.url
. An iframe is used to load in the same website the attack scenario. When a user clicks the Go back link, a XSS will be executed.
This is a simple case, in PostMessage Vulnerabilities. Part II real case scenarios will be analyzed.
Mitigations
According to Mozilla:
If you do not expect to receive messages from other sites, do not add any event listeners for message events. This is a completely foolproof way to avoid security problems.
If you do expect to receive messages from other sites, always verify the sender’s identity using the origin and possibly source properties. Any window can send a message to any other window, and you have no guarantees that an unknown sender will not send malicious messages. Having verified identity, however, you still should always verify the syntax of the received message. Otherwise, a security hole in the site you trusted to send only trusted messages could then open a cross-site scripting hole in your site.
Always specify an exact target origin, not
*
, when you use postMessage to send data to other windows. A malicious site can change the location of the window without your knowledge, and therefore it can intercept the data sent using postMessage.
In the previous scenario, the following code should be modified in 1.html:
child.postMessage(msg,'*')
to
child.postMessage(msg,'2.html')
And in 2.html:
window.addEventListener("message", (event)=>{
...
}
To:
window.addEventListener("message", (event)=>{
if (event.origin !== "http://safe.com")
return;
...
}
Detection
The way to detect postMessage vulnerabilities is reading JavaScript code. There is not an easy automated tool to help with this because when a listener is defined, the event data flow must be followed to analyze if the code ends in a vulnerable function. Anyways, I recommend two ways to detect the function calls:
With J2EEScan, from git repository, not BApp Store because I think is not updated:
With BurpBounty, defining a set of Passive response strings searching for keywords like: postMessage
, addEventListener("message
, .on("message"
.