After I learned about 4 types of implementing cross-domain scripting I thought that it would be nice to create a working demo. After all, I believe in “Learn By Example”. Besides since that article was published some new methods arrived, so why not write yet another tutorial.
Quick recap: you have two HTML pages located on the two different sites: http://www.a.com/page_a.html
and http://www.b.com/page_b.html
and you want page_a.html
to get some information from page_b.html
. For instance page_b
knows the first name of the person, who is browsing both page_a
and page_b
, and page_a
wants to use this information to say “Hi %firstname%”.
If they were located on the same domain, http://www.common.com/page_a.html
and http://www.common.com/page_b.html
it would be no problem. However, since they are on different domains, they cannot talk to each other, as that would violate same-origin policy, an important principle, that prevents some Russian script-kiddie from stealing your PayPal password in order to sell it on some South-african black market to some Chinese student who would use it to buy some marijuana from some Brazilian dealer.
The www.a.com
and www.b.com
can agree on some rules to make cross-site scripting between them possible and there are several main ways to do that:
- CORS (Cross-Origin Resourse Sharing)
- JSONP
- window.postMessage
- Local proxy
- MessageChannel
- Fragment Identifier Messaging
Before you continue I advise you to look at the easyXDM library which is absolutely recommended for using in real projects instead of building your own solution. This article is for those who want to understand the inner working of such libraries.
1. CORS (Cross-Origin Resource Sharing)
Let’s begin with three pair of people trying to talk to each other. Alice and Bob, Cindy and Douglas, Eve and Fred. Bob is not at home right now and is visiting Alice, so they are on the same domain. All other folks are on separate domains. First, the girls try to use ajax requests to get the messages from boys:
All of them used the same $.get method. Alice received her message successfully because Bob was on the same domain. Cindy failed to receive a message from Douglas who is on different domain. Eve had the same problem with Fred, but luckily Fred sends the HTTP header that allows Eve to use the data from Fred on her domain.
<?php header('Access-Control-Allow-Origin: https://eve.ruslanbes.com'); ?>
If you have Firebug or similar developer console, open it, and you will see something like that:
which tells you exactly what is wrong.
Notice that Fred only allows to use his message on Eve’s domain. If Cindy tries to steal Fred’s message, she would fail:
In extreme case Fred can set the header to:
<?php header('Access-Control-Allow-Origin: *'); ?>
meaning, that everyone can get the message.
Side-note: Dimitar Ivanov mentioned that ajax request to Fred does not use cookies by default. If Eve needs to allow Fred to use cookies when responding to her request, she has to use XHR with withCredentials
attribute and Fred must set "Access-Control-Allow-Credentials: true"
header. The cookies we are talking about are the ones on fred.ruslanbes.com and Eve cannot access them, only Fred can. Code sample, Demo from Mozilla documentation.
At the end of the first chapter we left Cindy unhappy, let’s look if she can find another way to communicate with Douglas.
2. JSONP (JSON Padding)
JSON Padding is an interesting technique that implements cross-site AJAX using a browser hack. It tries to exploit the fact that the browser can load JavaScript using the <script> tag from any domain. Sadly, unlike the usual AJAX you can load only JavaScript files in this way. Besides, when you include external script you don’t have any way to tell at which moment it was fully loaded, that is, you don’t have .done() function. So if the Eve has a tag like this:
<!-- www.eve.com/getmessage.html --> <script type="text/javascript" src="www.fred.com/json.js"></script>
and the www.fred.com/json.js
contains a JSON:
// www.fred.com/json.js { "name":"Fred", "message":"Hi" }
that would be the same as if Eve had this right from the start:
<!-- www.eve.com/getmessage.html --> <script type="text/javascript"> { "name":"Fred", "message":"Hi" } </script>
an anonymous object that she can’t access.
The technique is that instead of JSON Fred sends an instruction to call a JavaScript function and passes the JSON as a parameter:
<!-- www.eve.com/getmessage.html --> <script type="text/javascript"> function logMessage(fred) { console.log(fred.message) } </script> <script type="text/javascript" src="www.fred.com/jsonp.js"></script>
// www.fred.com/jsonp.js logMessage({ "name":"Fred", "message":"Hi" })
Again we need Fred and Eve to play together. Let’s check if other folks can make it. This time girls will use $.getJSON function.
In order to use JSONP Fred accepts the “callback” parameter from incoming URL. Eve knows that and adds "callback=?"
to the URL string. This magical XXXXX=?
part tells jQuery to use JSONP mode and it will substitute ?
with some not-used function name like jQuery211049818158980495376_1412848463345
.
If you look in the Firebug again you’ll notice something strange:
Where is the Eve’s request?
Here it is, on the NET tab.
You can check the name of generated callback there too.
Again, no luck for Cindy. This time Cindy can learn the technique from Eve and get the Eve’s message from Fred. Nothing can prevent that. In the next chapter we will use iframes for communication and check if Cindy can hack into them.
3. window.postMessage
This time parties will communicate through an iframe. Girls will host the initial pages and include boys using <iframe src="www.boy.com/page.html"/>
tags.
Again, for Alice and Bob there are no problems. But how to establish a connection between frames from different domains? Unfortunately "Access-Control-Allow-Origin"
has no effect here (although I don’t see the reason why not) and the frame contents are not JSON, so both CORS and JSONP are useless here:
Luckily we are living in year 2014 and window.postMessage is already invented. And this is exactly the right tool for the job. The idea is that sender frame calls the postMessage method of the receiver:
receiver_iframe.postMessage({"name":"Sender", "message":"Hi"}, 'http://www.receiver.com'); // not the correct code
and the receiver should register a handler for messages:
$( window ).on("message", function( event ) { if (event.origin !== "http://www.sender.com") return; // not the correct code console.log(event.data.name + " says: " + event.data.message) })
This is in theory. In practice we will need to add some more code, but let’s look at the demo:
Okay. Alice can simply query $("#bobs_iframe").contents()
and get any information she needs. Cindy does that too, but in cross-domain mode the contents()
will be empty. The most interesting part is with Eve.
Eve uses following line:
$("#freds_iframe").get(0).contentWindow.postMessage("How about a lunch?", 'https://fred.ruslanbes.com')
The get(0)
is needed to get the iframe object. Using the .contentWindow
property we get limited access to the iframe’s window
object and execute postMessage()
call.
Fred is ready to receive it:
$( window ).on("message", function( event ){ event = event.originalEvent // ??? if (event.origin !== "https://eve.ruslanbes.com") return; parent.postMessage( $("#message_to_eve").text(), "https://eve.ruslanbes.com" ); })
okay, what is line 2 doing here?
The thing is that jQuery has internal event handler that is executed before a JavaScript event is passed to a target. It does some browser-fixing stuff and passes event mostly with its original properties, but at this moment (jQuery 2.1.1) it does not properly handle the "message"
event. We use event.originalEvent
to extract it.
The rest is as expected, Fred sends the message back to Eve using Eve’s window (parent
). Eve receives it.
// Eve $( window ).on("message", function( event ){ event = event.originalEvent if (event.origin !== "https://fred.ruslanbes.com") return; $("#freds_message").val(event.data) })
Some notes about this mechanism. First the message itself is better to be a string or number, but not an Object. The problem with Object is that it is not supported up to IE 9. If you still need it, use a workaround:
// sender.com // ... postMessage( JSON.stringify( {"name":"Sender", "message":"Hi"} ), "http://receiver.com" ) // ... // receiver.com // ... var data = jQuery.parseJSON( event.data ) // ...
Second is that you have to include the target URL with every request and check it manually, which can matter if you have hundreds of messages going simultaneously.
If you open Firebug and click the Eve’s button you can see additional debug messages:
One thing is worth mentioning. When Eve sends a message she uses Fred’s window: $("#freds_iframe").get(0).contentWindow.postMessage( ... )
, but when she wants to receive the message, she registers handler on her window: $( window ).on("message", ... )
.
Same thing with Fred. Fred sends the message to Eve’s window: parent.postMessage
, but handles the message on his frame.
But what if we changed the composition of the frames and Eve is now not parent
but parent.parent
or even parent.parent.parent.$("#eves_iframe")
. Obvious question: can we put all the handling/receiving logic into one frame? For example the top
since “top” is always available. Let’s try it.
Eve’s code is changed to:
// Eve top.postMessage("How about a lunch?", 'https://fred.ruslanbes.com') // and $( top ).on("message", function( event ){ event = event.originalEvent if (event.origin !== "https://fred.ruslanbes.com") return; $("#freds_message").val(event.data) })
Fred’s:
// Fred $( top ).on("message", function( event ){ event = event.originalEvent if (event.origin !== "https://eve.ruslanbes.com") return; $( top ).postMessage( $("#message_to_eve").text(), "https://eve.ruslanbes.com" ); })
Let’s check it:
Nope, it doesn’t work and if you open Firebug you’ll see a strange error:
This message is raised by Eve’s and Fred’s attempts to register onMessage event on the “top” frame which belongs to neither’s domain. This is a nasty limitation, but we have to live with it for now.
Nice, we now know 3 methods and have angry Cindy, who thinks nobody wants to talk to her. Time to fix it.
4. Local proxy
This by far the easiest, most powerful and, if implemented improperly, most insecure way to circumvent cross-domain policy. First of all it does not require to change anything in the second party’s code. The first party sets a simple proxy like this:
<?php $url = $_GET['url']; $parsed = parse_url($url); if ($parsed['host'] !== 'douglas.ruslanbes.com') { header('HTTP/1.0 403 Forbidden'); die("Forbidden"); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); curl_close($ch); echo $output;
This proxy looks at the parameter “url”, checks if it belongs to Douglas and sends the request by itself. So Cindy can now send requests like: "https://cindy.ruslanbes.com/proxy/proxytodouglas.php?url="+ encodeURI("https://douglas.ruslanbes.com/cors/douglas.php")
.
Note of caution: Always check the url your proxy is working with. Second note: if you check it with RegExps, always do a proof-test with requests like this "https://cindy.ruslanbes.com/proxy/proxytodouglas.php?url="+ encodeURI("http://www.attackthissite.com/?param=https://douglas.ruslanbes.com")
Okay, testing.
Works fine. Cindy finally managed to get the message.
5. MessageChannel
The Channel Messaging is a new technique that is meant to fix some problems with postMessage. Unfortunately it is completely unsupported by Internet Explorer up to IE9 and Firefox up to FF35.
Let’s rewrite the Cindy’s and Douglas’s iframe code to use message channels:
// Cindy $("#get_douglass_message").click(function() { if (window.MessageChannel) { // check the support of MessageChannel if (typeof window.myMessageChannel == 'undefined') { // when you click the button first time console.log("Cindy: Establishing channel with Douglas") window.myMessageChannel = new MessageChannel(); // Create MessageChannel object window.myMessageChannel.port1.onmessage = function( event ) { // attach listener for messages console.log("Cindy: I got message from Douglas throught the port") $("#douglass_message").val(event.data) } var w = $("#douglass_iframe").get(0).contentWindow w.postMessage("Hi Douglas", 'https://douglas.ruslanbes.com', [window.myMessageChannel.port2]) // send Douglas the second port of the channel $( '#get_douglass_message' ).text( "Say 'Hi again' to Douglas" ) } else { console.log("Cindy: Sending message through the port") window.myMessageChannel.port1.postMessage("Hi again") } } else { $("#douglass_message").val("<Failed: MessageChannel unsupported>") } })
Lots of code here, let’s check what’s going on. When you click the button first time, lines 4-13 are used. We create MessageChannel object and save it as window.MessageChannel
. At line 12 we use the same postMessage function but with third parameter transfer. The transfer parameter is an array of Transferable objects. When we send them this way, the sender (Cindy) loses control over them, and the receiver (Douglas) gains it. Cindy sends window.myMessageChannel.port2
– that means Cindy cannot do something like window.myMessageChannel.port2.onmessage = function() {alert("hi")}
anymore.
After the connection is established and button is clicked again, Cindy uses lines 15-16. Note that she uses the same postMessage
but this time she sends only the message and nothing else.
Let’s look at the Douglas’s code:
// Douglas $( window ).on("message", function( event ){ console.log("Douglas: I got message from someone") event = event.originalEvent if (event.origin !== "https://cindy.ruslanbes.com") return; console.log("Douglas: It's from Cindy, she says: " + event.data) console.log("Douglas: Attaching message handler to port from Cindy") var portToCindy = event.ports[0]; portToCindy.onmessage = function( event ) { console.log("Douglas: I got message from Cindy through the port") this.postMessage("Hi again, Cindy") } portToCindy.postMessage("Hi, Cindy. Channel established") })
So, Douglas get the port2
from the initial postMessage: var portToCindy = event.ports[0]
, then attaches the onmessage handler and sends the message using portToCindy.postMessage
. Let’s check. Remember, you should not use Firefox < 35 or IE < 10:
Works. If you look at the console, you’ll see a following dialog:
6. Fragment Identifier Messaging
This method was probably the first one used to fight the SOP for the iframes case. Recall that “sender” window have a limited access to the “receiver” window and can even change some things inside. One of the things it can actually change is the window.location
attribute, that is, URL of a window. So the trick is to paste the message into this attribute and let the window/iframe read it.
Now if you change the location to a completely different page the window will reload. So how can you change it so that the window is not reloaded?
Easy. Simply change what goes after “#”(hash) sign.
Unfortunately since we are using the thing that was not supposed for messaging, we should be ready for some limitations. The most important is that all browsers have some limit for what they think you can put as a URL. Most of the browsers has this limit at about 100 000, but IE has a limit at 2083 chars which is kinda frustrating. Let’s try to build the demo:
Well, it probably does not work. In Firefox it raises NS_ERROR_DOM_PROP_ACCESS_DENIED error because Firefox thinks that having iframe inside an iframe and playing with their location is way too much for a normal script. Other browsers have problems too. It should work however if you open the Cindy’s home directly: https://cindy.ruslanbes.com/fragment/cindy.php.
Let’s look at the code of Douglas (the code of Cindy is very similar):
// Douglas window.addEventListener("hashchange", getMessageFromCindy, false) // track the hash change event function getMessageFromCindy() { var hash = window.location.hash.replace(/^#/,'') // get what goes after the has sign console.log("Cindy says:" + hash) parent.location = "https://cindy.ruslanbes.com/fragment/cindy.php" + "#" + $("#message_to_cindy").text() // send a message back using hash }
Some notes about this solution.
Note 1: We cannot check if the parent message comes from Cindy. The parent.location
is write-only property, so we can only hope it is Cindy and not Boris or someone like him :)
Note 2: We have to know the initial parent window address. If it’s not https://cindy.ruslanbes.com/fragment/cindy.php
then the whole communication will not work. One way to workaround this is to pass the parent address into the hash along with the message json-encoded, like {"callback":"https://cindy.ruslanbes.com/fragment/cindy.php", "message":"Hi, Douglas"}
.
Summary
Small cheat-sheet listing all methods:
Method | Advantages | Disadvantages | Support |
---|---|---|---|
CORS |
|
| All modern browsers, except Opera Mini. |
JSONP |
|
| All browsers which support JavaScript. |
window.postMessage |
|
| All modern browsers. IE 8 and 9 — partially |
Local proxy |
|
| All browsers. |
MessageChannel |
|
| Most browsers except: IE <= 9, Firefox <= 35, Opera mini, Android browser <= 4.3 |
Fragment Identifier Messaging |
|
| All modern browsers except Opera Mini. There are more limitation if both parties are iframes. |
The source code of the demo is on Github: ruslanbes/crossdomain
I’m not sure exactly why but this weblog is loading very slow
for me. Is anyon else having this issue or iss it a isue on myy end?
I’ll check back later and see if the problem still exists.
Thanks for the info. Noticed it too. This post may be slower than the others since I load the iframes using setTimeout to force the sequential load.
But probably it’s anyway the time to seek a new hoster :)
Very helpful and detailed post, thanks. However, see how to use the withCredentials property: http://zinoui.com/blog/cross-domain-ajax-request
Added that to post. Thanks