Posted by on 2 May 2014 in Security, Software | 0 comments

A couple of months ago I found a vulnerability on Microsoft’s website.


Modern web browsers severely restrict what content loaded from different “origins” can access about each other. Without these restrictions, a web page containing malicious code could simply connect to another site in the background – say Facebook, or your bank – and access everything on that site as if it were you doing so. Conversely, many modern web applications use a variety of workarounds to enable some limited cross-origin communication.

In light of these workarounds, HTML5 introduces a new JavaScript API, window.postMessage. This function provides a secure and standardised way for scripts in different windows – and potentially different origins – to send messages to each other.


While the channel of communication is secure, scripts are of course free to do whatever they wish with messages they receive. This makes it quite possible to shoot oneself in the foot.

While browsing the HTML source of a few pages on, I noticed that the JavaScript code there made regular use of postMessage to send data to a parent window. It didn’t appear that the code checked what the origin of the parent window was, so an attacker could embed one of these pages in a frame and listen for these messages from it. Nothing especially concerning, but I wondered if there was anywhere in the code that received messages from other windows and acted on them.

It didn’t take long to find that there was, on the “edit payment option” page, on a URL like this:

Normally that “Id” value would be a long number, specific to my account. Helpfully, for exploitation purposes, providing any “Id” value at all in the URL resulted in a page like the below:

That page was interesting because the large area with the white background was actually embedded from a different page entirely. Not only that, but JavaScript loaded on the top level page – from – listened for incoming messages and acted on them. One of the messages was pcs_onnavigationrequired, dealt with by the following code:

//pcs_onnavigationrequired - sent by PCS to the host document
//when PCS needs to navigate to another page such as Paypal.
this.pcs_onnavigationrequired = function (postmessagemodel) {
    pcsIframe.attr('src', postmessagemodel.redirecturl);

When that message is received, the code happily navigates the embedded iframe (the white area in the screenshot above) to whatever URL was sent in the message.

The origin of the message was not checked, so that message could come from anywhere. Likewise the destination URL could be completely arbitrary too. At the very least this would make for a very convincing looking phishing page – the URL bar would say “”, the header would show the username (“gamertag”) that the user was expecting, but most of the content on the page could be coming from anywhere.

Proof of Concept

It was straightforward to put together a proof-of-concept exploit to demonstrate the problem.

  1. Attacker directs the user to the EditPaymentOption page.
  2. Attacker sends a message to the window that contains this page, instructing it to navigate the <iframe> to a page controlled by the attacker.
  3. User sees no obvious visual evidence that this has occurred; assuming that the attacker-controlled page has been loaded over HTTPS, the browser will continue to display a padlock symbol (or similar) and the address bar will show the original URL.

Sample exploit code:

<script type="text/javascript">
function explDemo()
    var w ='', '_expl');
    setTimeout(function() {
                "type": "message",
                "data": JSON.stringify({
                    "method": "pcs_onnavigationrequired",
                    "redirecturl": ""
            }), '*');
    }, 10000);

    return true;
<a href="" target="_expl" onclick="explDemo()">Edit Payment Options</a>

The sample exploit is a plain HTML containing a link. When that link is clicked,’s “Edit Payment Options” page is opened in a new window (or more likely, tab).

Assuming the exploit window isn’t closed in the meantime, ten seconds later, the window is sent a specially crafted message. That message causes the inner frame of that window to be navigated to I chose that as a sort of "nothing up my sleeve" URL – there’s nothing special about it; I just wanted to use an HTTPS URL to keep the browser happy that everything is still secure.

Here’s an example of how it looked, a few seconds after clicking the link:

Imagine that the content in the middle was asking you to update your credit card details.

Bonus Licence Issue

The most obvious potential fix would be for the code to only act on incoming messages after checking their origin is “” or similar. That it didn’t already do this was especially perplexing, given that this bit of code was also present:

hash = $.parseJSON(decodeURIComponent(hash));
if (!(hash['x-requested-with'] === 'postmessage' &&
    hash.source && != null
        && hash.source.url && hash.postmessage)) {
    // ignore since hash could've come from somewhere else

This bit of code wasn’t really used (another odd point) but it implies that the dubious origin of messages was at least considered by the authors. This didn’t make much sense to me, until I Googled that “ignore since…” comment and found that the entire block of code – and much of the rest of the file – is directly copied from the MIT licensed postmessage.js.

Microsoft’s Pcs.js looked extremely similar, except it contained no mention of the licence. At the time of writing, it still doesn’t. This despite the “The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.” in the original file.

I reported the security and licence issues to Microsoft. The security issue was fixed by adding this code to check the origin of received messages:

var reXbox = /^https:\/\/live.(?:\w+\.)*$/i;
var reMs = /^https:\/\/buy\.microsoft(?:-int)?\.com$/i;
var reLive = /^https:\/\/buy\.live(?:-int)?\.com$/i;
if (!e.origin || (!reLive.test(e.origin) && !reMs.test(e.origin) && !reXbox.test(e.origin))) {


18/Nov/2013 Issue reported to vendor
18/Nov/2013 Report acknowledged by vendor and case number assigned
19/Nov/2013 Licence issue reported
27/Nov/2013 Thanked for reporting both issues by case manager
07/Jan/2014 Case manager reports that issue is now fixed
08/Jan/2014 Replied stating that issue appears still present
09/Jan/2014 (Security issue appears to have been resolved on site)
19/Feb/2014 Vendor requests details for acknowledgement