Cross-Document Messaging
As a general rule, scripts loaded by web content served from one origin (host and domain) cannot access web content served by a different origin. This is an important security feature that prevents a multitude of different security attack vectors. However, it also makes it difficult for scripts to interact with one another across these boundaries.
To make communication between documents from different origins easier, the HTML 5 specification adds cross-document messaging. This feature is supported in Safari 4.0 and later.
Posting a Message to a Window
To post a message, you must first obtain the Window
object of the document you want to message. In effect, this means that you can post messages only to:
other frames or inline frames within your document window (or their descendants if all intermediate frames or inline frames were served from the same origin).
var iFrameObj = document.getElementById('myId');
var windowObj = iFrameObj.contentWindow;
windows that your document explicitly opened through JavaScript calls.
var windowObj = window.open(...);
the window that contains your document window, the window that contains that window, and so on up to the root window.
var windowObj = window.parent;
the window that opened your document.
var windowObj = window.opener;
Once you have obtained the Window
object for the target document, you can send it a message with the following code:
windowObj.postMessage('test message', 'http://example.com');
The first parameter is an arbitrary message.
The second parameter is the target origin value. An origin value is just a URL with the path part removed. For example, the origin of a local file is file://
. By specifying a target origin, you are saying that that your message should only be delivered if the target window’s current contents came from that origin.
Although you may specify an asterisk (*
) wildcard for the target origin (to allow the message to be sent regardless of where the contents of the target window came from), you should do so only if you are certain that it would not be harmful if your message were received by content originating from a different website.
Receiving a Message Posted to a Window
To receive messages, you must add an event listener for the message
event type to your document’s window
object. To do this, use the following code:
function messageReceive(evt) {
if (evt.origin == 'http://example.com') {
// The message came from an origin that
// your code trusts. Work with the message.
alert('Received data '+evt.data);
// Send a message back to the source:
evt.source.postMessage('response', evt.origin);
} else {
alert('unexpected message from origin '+evt.origin);
}
}
window.addEventListener('message', messageReceive, false);
The message
event you receive has three properties of interest:
data
—the message contents.origin
—the domain from which the message was sent (http://example.com
in this case).source
—the window from which the message was sent.
A Service Discovery Example: Message Boxes
This section contains two code listings: index.html
and msg_contents.html
that, when combined, implement basic service discovery and data relaying on top of the cross-document messaging architecture.
To use this code, you must first do the following things:
Install Safari 4.0 or later, or install a recent WebKit nightly.
Save the contents of the two code listings in separate files.
In the file
msg_contents.html
, modify the variableallowed_origins
to contain a list of origins from which you plan to serve theindex.html
ormsg_contents.html
file.For example, if you intend to place this file at
http://www.example.org/message_test/msg_contents.html
, you should make sure that'http://www.example.org'
(enclosed in quotes) is a key in theallowed_origins
object.If you only have one machine, turn on web sharing, then use
http://localhost
as one origin andfile://
as the other.In the file
msg_contents.html
, optionally change the variableroot_origin
to the actual expected origin of the top level HTML page.Place a copy of the modified
msg_contents.html
file on the desired server or servers.In the file
index.html
, replace theallowed_origins
declaration with the one from yourmsg_contents.html
file. Then, update theboxes
object to provide the URLs for themsg_contents.html
files you just put on your servers.If you only have one machine, use a
file://
URL pointing to the path of themsg_contents.html
file. For example, if you placed the file in/Library/WebServer/Documents/message_test/msg_contents.html
, the local URL would befile:///Library/WebServer/Documents/message_test/msg_contents.html
.Place this modified
index.html
file on one of the servers and navigate to the URL, or open it as a local file on disk.
Once you have completed these steps, you should see several boxes, one per entry in the boxes
object. Each of these boxes should contain a small form with a text input box, a series of checkboxes (one for each of the outer boxes), and a submit button.
If you type something into the text box, check one of the checkboxes, and click submit, the text should appear at the bottom of the window whose name corresponds with the checkbox.
<html><head>
<script language='javascript' type='text/javascript'><!--
/*global alert, navigator, document, window */
var window_list = [];
var origin_list = [];
var boxes = {
local: "http://host1.domain1.top/messages2/msg_contents.html",
remote: "http://host2.domain2.top/messages2/msg_contents.html",
third: "http://host3.domain3.top/messages2/msg_contents.html"
};
var allowed_origins = {
'http://host1.domain1.top': 1,
'http://host2.domain2.top': 1,
'http://host3.domain3.top': 1
};
function smartsplit(string, pattern, count)
{
// alert('string '+string);
// alert('pattern "'+pattern+'"');
// alert('count '+count);
var lastpos = count - 1;
var arr = string.split(/ /);
// alert('AC: '+arr.length+" "+string);
if (arr.length > lastpos) {
var temparr = [];
for (var i=lastpos; i<arr.length; i++) {
temparr[i-lastpos] = arr[i];
arr[i] = undefined;
}
arr[lastpos] = temparr.join(pattern);
}
return arr;
}
function listWindows()
{
var retstring = "";
for (var i in origin_list) {
if (origin_list.hasOwnProperty(i)) {
// alert('UUID: '+i+' Origin: '+origin_list[i]);
retstring += i+' '+origin_list[i]+'\n';
}
}
return retstring;
}
function messageReceive(evt) {
var windowlist;
if (evt.origin === null || evt.origin in allowed_origins) {
var arr = smartsplit(evt.data, " ", 2);
if (arr[0] == 'sendto') {
// usage: sendto UUID remote_origin message
arr = smartsplit(evt.data, " ", 4);
var remote_window = window_list[arr[1]];
remote_window.postMessage('sendto_output '+arr[3], arr[2]);
} else if (arr[0] == 'register') {
// usage register UUID
var name = arr[1];
if (window_list[name]) {
// name conflict.
var add=1;
while (window_list[name+'_'+add]) {
add++;
}
name = name+'_'+add;
evt.source.postMessage("register_newid "+name, evt.origin);
}
window_list[name] = evt.source;
origin_list[name] = evt.origin;
windowlist = listWindows();
for (var windowid in window_list) {
if (window_list.hasOwnProperty(windowid)) {
// alert('windowid: '+windowid);
window_list[windowid].postMessage("list_output "+windowlist, origin_list[windowid]);
}
}
} else if (arr[0] == 'list') {
// usage list
windowlist = listWindows();
evt.source.postMessage("list_output "+windowlist, evt.origin);
} else {
alert('unknown command '+arr[0]);
}
} else {
alert('unexpected message from origin '+evt.origin);
}
}
function dosetup()
{
if (navigator.userAgent.match(/Safari/)) {
var version = parseFloat(navigator.userAgent.replace(/.*AppleWebKit\//, "").replace(/[^0-9.].*$/, ""));
if (version < 528) {
alert('WebKit version '+version+' does not support this application.');
}
}
// for (var i=0; i<nchannels; i++) {
// alert('setup channel '+i);
// channel[i] = new MessageChannel();
// }
window.addEventListener('message', messageReceive, false);
var boxstr = "";
for (var i in boxes) {
if (boxes.hasOwnProperty(i)) {
boxstr += "<iframe height='600' id='"+i+"' src='"+boxes[i]+"'></iframe>\n";
}
}
var boxlistdiv = document.getElementById('boxlist');
boxlistdiv.innerHTML = boxstr;
}
dosetup();
--></script>
</head>
<body onload='dosetup();'>
<div id='boxlist'>
</div>
</body>
</html>
<html><head>
<script language='javascript' type='text/javascript'><!--
/*global alert, document, window, navigator */
var allowed_origins = {
'http://stavromula-beta.apple.com': 1,
'http://holst.apple.com': 1
};
var received_root_origin = '';
var root_origin = '*';
function smartsplit(string, pattern, count)
{
// alert('string '+string);
// alert('pattern "'+pattern+'"');
// alert('count '+count);
var lastpos = count - 1;
var arr = string.split(/ /);
// alert('AC: '+arr.length+" "+string);
if (arr.length > lastpos) {
var temparr = [];
for (var i=lastpos; i<arr.length; i++) {
temparr[i-lastpos] = arr[i];
arr[i] = undefined;
}
arr[lastpos] = temparr.join(pattern); }
return arr;
}
function mkcheckbox(inpstr)
{
var str = "";
var arr = inpstr.split("\n");
for (var entid in arr) {
if (arr.hasOwnProperty(entid)) {
var ent = arr[entid];
if (ent !== "") {
// alert('ent: '+ent);
var bits = smartsplit(ent, " ", 2);
str += "<input type=checkbox name='"+ent+"'>"+bits[0]+"</input>\n";
}
}
}
return str;
}
function messageReceive(evt) {
if (evt.origin === null || evt.origin in allowed_origins) {
// The message came from an origin that
// your code trusts. Work with the message.
// alert('Received data: '+evt.data);
// var tmp = 'Test this, please';
// var x = smartsplit(tmp, " ", 2);
// alert('x[0] = '+x[0]);
// alert('x[1] = '+x[1]);
// alert('x[2] = '+x[2]);
var arr = smartsplit(evt.data, " ", 2);
if (arr[0] == 'list_output') {
if (evt.origin == root_origin || root_origin == '*') {
var div2 = document.getElementById('temp2');
div2.innerHTML = mkcheckbox(arr[1]);
received_root_origin = evt.origin;
// alert('arr[1] = '+arr[1]);
} else {
alert('received list_output message from unexpected origin: '+evt.origin);
}
} else if (arr[0] == 'sendto_output') {
// alert('Received data: '+evt.data);
var div3 = document.getElementById('temp3');
div3.innerHTML += arr[1]+'<br />\n';
} else if (arr[0] == 'register_newid') {
var mydiv = document.getElementById('temp');
mydiv.innerHTML = arr[1]+" box";
}
// Send a message back to the source:
// evt.source.postMessage('response', evt.origin);
} else {
alert('unexpected message from origin '+evt.origin);
}
}
function setup_listener()
{
window.addEventListener('message', messageReceive, false);
}
function setup()
{
var mydiv = document.getElementById('temp');
// mydiv.innerHTML = 'Test';
// mydiv.innerHTML = ' '+bigdoc;
// If we can access the document object, we are locally loaded and should
// talk to the remotely-loaded window. Otherwise, the reverse
// is true.
var myid = '';
if (window.parent.document) {
myid = 'local';
} else {
myid = 'remote';
}
mydiv.innerHTML = myid+" box";
// window.addEventListener('message', messageReceive, false);
setup_listener();
var topwindow = window;
while (topwindow.parent && (topwindow.parent != topwindow)) { topwindow = topwindow.parent; }
topwindow.postMessage('register '+myid, root_origin);
// window.parent.postMessage('list', '*');
}
function sendmsg()
{
var message = document.getElementById('sendtext').value;
var mydiv2 = document.getElementById('temp2');
var checkboxes = mydiv2.children;
// alert('checkboxes: '+checkboxes);
var topwindow = window;
while (topwindow.parent && (topwindow.parent != topwindow)) { topwindow = topwindow.parent; }
for (var boxid in checkboxes) {
if (checkboxes.hasOwnProperty(boxid)) {
var checkbox = checkboxes[boxid];
// alert('checkbox: '+checkbox);
if (checkbox.tagName == "INPUT") {
// alert('input');
if (checkbox.checked) {
// alert('checked');
var arr = smartsplit(checkbox.name, " ", 2);
var uuid = arr[0];
var origin = arr[1];
// alert('send to: '+uuid+' origin '+origin);
// alert('topwindow is '+topwindow);
topwindow.postMessage('sendto '+uuid+' '+origin+' '+message, received_root_origin);
}
}
}
}
return false;
}
--></script>
</head><body onload='setup();'>
<div id='temp'></div>
<form onsubmit='return false;'>
<input type='text' id='sendtext'></input>
<input type='submit' value='submit' onclick='sendmsg();'></input>
<div id='temp2'></div>
</form>
<div id='temp3'></div>
</body></html>
You can create numerous interesting extensions on top of this sort of design. For example, you might add a command message that asks the window at the other end what commands it supports, then communicate with it if you share a common set of commands. The possibilities are limitless.
Security Considerations
There are several key things you should be aware of when using cross-document messaging:
Obtaining
Window
objects for other windows is not always easy. Scripts running in a window, frame, oriframe
element served from one origin cannot access the DOM tree of documents served from a different origin, and thus cannot get access to theWindow
objects of otheriframe
elements within such a window.Among other things, this means that two
iframe
elements from different domains cannot directly obtain each other’sWindow
objects because one or the other is (by definition) from a different origin than the document containing theiframe
elements. In this situation, there are two possible solutions.The easiest solution is to have both windows discover each other using the parent window as a communications hub. This solution is also the most general solution because it works even if neither window can see the element containing the other window.
Alternatively, the window with access to the parent window’s DOM tree could discover the other one and initiate communication. By doing so, the second window receives the first window’s
Window
object by way of thesource
field in the event object.Sending data to other windows can be dangerous, particularly if that information contains login information or other sensitive data. You should almost always take advantage of the target origin field when sending messages to avoid interception by content served by other sites. You should only use the wildcard asterisk (
*
) target origin value if you are absolutely sure that the data is harmless.Receiving data from other windows can also be dangerous. You should generally check the origin field when receiving messages to make sure that the data was sent from a website that you trust to some degree.
Where possible, you should also check any received data for validity before using it. In particular, you should generally avoid executing JavaScript code received from another window (with the possible exception of JSON objects after careful validity checking).
As with any software, for maximum reliability and security, you should write your output code carefully to minimize the risk of causing problems for other code, and you should write your code under the assumption that other code is maliciously trying to attack your code, and thus you should perform type, bounds, and other sanity checks accordingly.
Copyright © 2018 Apple Inc. All rights reserved. Terms of Use | Privacy Policy | Updated: 2017-09-19