Security Risks Arise From Insecure Implementations of HTML5 postMessage() API

In this post we are going to have a look at the security risks arising due to insecure implementation of the HTML5 postMessage()API. Before we discuss how this cross-domain messaging API works, we must understand a few important concepts such as the same-origin policy and security risks associated with cross-origin communications.

Same-origin policy

The origin of a page is decided by three unique factors: hostname, protocol, and port number. For example, http://test.com and https://test.com have different origins because the protocol is different. Similarly, http://one.test.com and http://two.test.com have different origins because the hostnames are different. The origin property is also different for two services running on the same host with different port numbers, for example, http://test.com:8081 and http://test.com:8082 are different origins.

The same-origin policy (SOP) is a browser-level security control that dictates how a document or script belonging to one origin can interact with a resource from some other origin. Basically, SOP prevents scripts running under one origin from reading data from another origin. Cross-domain requests and form submissions are still permitted, but reading data from another origin is not permitted. SOP does not prevent resources hosted on different domains from being embedded in a page by using <script> tags, cascading style sheets, and <img> tags.

In a world without SOP, the Internet would not be very safe. Imagine you are logged into your bank’s website and simultaneously are accessing a news site in another tab. If the news site can read data from your bank’s site, you do not have to be a security expert to understand the risk.

Need for cross-origin communication

SOP has done a good job protecting users from unauthorized cross-domain data access, though like many other security controls it does not boost usability. The Internet has evolved beyond individual websites serving content and has become more distributed. The need arose to enable secure cross-domain communication to allow services hosted on different domains to communicate with each other. The postMessage() API, introduced in HTML5, tries to provide a safe mechanism. (There are other methods for cross-domain communication such as using HTTP response headers, but we will not discuss those here.)

Cross-origin messaging

The Window.postMessage() method, introduced in HTML5, allows JavaScript code running on different origins to communicate with each other in a bidirectional manner. This API can be used for communication between an iframe and its parent document. Similarly, it can be used by an HTML page and a child window to exchange messages, such as an embedded third-party video notifying its parent frame when the user pauses the video. Let’s look at some code snippets to better understand how cross-origin messaging works.

Consider an HTML page hosted on http://www.test.com that contains an iframe element pointing to http://www.child-frame.com. The parent frame can use the postMessage() call on the window object of the iframe to send a message.

//The following JavaScript code will be part of the parent document
//iframe example
var iframe = document.getElementsByTagName(‘iframe’)[0];
iframe.contentWindow.postMessage(“hello”, “*”);
——————————–OR—————————————–
// pop-up window example
var ref = window.open(“http://childframe.com”);
ref.postMessage(“hello”, “*”); 

In the preceding code snippet the iframe element is fetched and in the next step the contentWindow property of the iframe is accessed, which returns a window object reference. The postMessage() call contains two parameters, the message string and the target domain.

In the pop-up window example, in which messages are to be exchanged between the parent document and a child pop-up window, the reference returned by window.open() can be used to call the postMessage() API.

//The following JavaScript code will be a part of the child frame
window.addEventListener(‘message’, msgHandler);
function msgHandler(event)

{
// The sender’s origin and data received are displayed
alert(event.origin+‘says:’+ event.data);
}

On the receiving end, we need to have an event listener to listen for an incoming message. The preceding msgHandler() method is triggered when an event is received. The dispatched message contains certain properties that can be accessed by using the event object reference:

  • event.data: Object sent from the sender window (arrays, strings, numbers and other JavaScript objects are supported)
  • event.origin: Origin of the sender window
  • event.source: A reference to the window object of the sender window. This can be used to send a message to the sender window.

Risk analysis and protection measures

Let’s look at a couple of elements from a developer’s perspective:

  • It is important to specify the origin of the target window while sending a cross-origin message to ensure that the message is received by the intended recipient.
  • On the receiving end, it is important to validate the origin of the sender to check the integrity of the message received.
  • Using the received data in the client-side logic without validation may open doors for script-injection attacks.

The API specification provides very clear guidelines to developers for securely using the postMessage()API. (See this link to the specification.) The code snippets shown earlier do not follow these best practices while implementing the postMessage() API. Let’s look at the compliant code snippets.

//The following JavaScript code will be part of the parent document
var iframe = document.getElementsByTagName(‘iframe’)[0];
//The postMessage call specifies the target origin rather than using wildcards
iframe.contentWindow.postMessage(“hello”, “http://www.child-frame.com”); 

The preceding code explicitly mentions the target origin. The previous example used a wild card, which offers no guarantee that the message will be delivered to the intended origin. This omission may result in a vulnerability when an iframe or a pop-up tries to communicate with its parent because a malicious site could open a legitimate website in a pop-up or iframe.

Similarly, on the receiving end, the authors should check if the message is received from an expected origin. Along with origin verification, it is important to perform input validation on the data received from the other domain before using it in the client-side logic. The compliant code solution for the receiving end follows:

//The following JavaScript code will be a part of the child frame
window.addEventListener(‘message’, msgHandler);
function msgHandler(event)

{
// The sender’s origin is validated
if (event.origin == “http://www.test.com”)

{
if(event.data == “hello”)

{
//Do some action after validating the message content
}

}

}

Demonstration of an attack

Consider the (fictional) credit card rewards program website https://www.acmerewards.com, which is running a promotional campaign. Customers are asked to play a quick game of Sudoku with the week’s highest scorers receiving reward points. The game is hosted on https://play.acmerewards.com. This URL opens as a pop-up from the main site’s home page.

The link to this game is shown to customers after they are authenticated. Once the pop-up opens, the main site passes the name of the current user to the pop-up window. The game window uses this to create a welcome message.

The vulnerability arises because the pop-up window assumes that it will always receive the message from www.acmerewards.com and thus does not validate the sender’s origin and the contents of the message. Because it is possible to open the pop-up window from any HTML page, the post message can be sent by any malicious domain. The message can contain malicious JavaScript code, which could run in the context of http://play.acmerewards.com, resulting in script injection that can be exploited in many ways. We will use a locally deployed application to demonstrate this attack. The initial screenshots show the expected application behavior. The final screen shows the script-injection attack using a post message.

The home page (www.acmerewards.com) of the rewards application. Clicking the PLAY button opens a pop-up window.

The pop-up window opens (play.acmerewards.com). Clicking OK in the sender window (www.acmerewards.com) sends the post message to the pop-up window. The two windows have different origins.

The string “username” is sent as a message from the sender window and is used to create a welcome message by the pop-up window.

We see the final exploit scenario in which a malicious website (www.realbadhacker.com) opens a pop-up window that points to a legitimate URL and passes a cross-site scripting payload instead of the username in the message. This script runs in the context of the target domain, which is trusted by the user.

The attack takes place completely on the client side; there is no interaction with the server. This “client XSS” attack avoids any server-side control that might have helped detect or prevent the attack. Both the source and sink of this attack occur on the client, and the injected malicious scripts are never sent to the server. The vulnerability should be fixed in the client-side code by validating the sender’s origin and data received.

The code for this demo application can be found here. Code samples provided in this post have been tested with Mozilla Firefox Version 52.0.1.

Reference

https://html.spec.whatwg.org/multipage/comms.html#web-messaging

Leave a Comment

four × 1 =