A note on JSONP & misconceptions of Cross Origin AJAX

Web developers who have worked on accessing APIs using JavaScript would be very much familiar with the term “JSONP”. Many web devs whom I have met offline or in online discussion forums seem to have some misconceptions about JSONP. Below are some of the basic & common definitions which I have come across:

  • JSONP is a technique to work with remote APIs
  • It is nothing but Cross Origin AJAX
  • If we add a query string like “?callback=someCallback” and fire jQuery’s $.ajax or $.getJSON, what we are doing is nothing but a JSONP call.
  • May be a slightly complicated definition: Cross origin AJAX is possible only when the response thrown is JavaScript
  • and many more..

The truth in the above statements is very little and such definitions add more confusion, bringing in misconceptions. In my recent presentation “Content Isolation with Same Origin Policy”, I put up the below slides (check slides 4 & 5 in the ppt)

image

image

 

 

 

 

 

 

 

 

 

 

For all practical purposes, the first one is possible and second one is not. Apart from the tweaked definitions of JSONP as stated above, the below reasoning complicates the topic:

  • In the first case, the content requested is of the type “text/javascript” while in the second case it is HTML. So browsers look at content type of the response header and decide whether they should block the content or not (actually, a very good observation).
  • There is a “?callback=?” parameter in the first case enables jQuery to make the cross origin call in the first case
  • Server side framework should have special capabilities (Iike inbuilt serialization/deserialization) for the first case to work

I thought it would be nice to summarize few facts and hence this post. Read on.

What's an Origin?

The combination of scheme://host:port is what browsers treat as an Origin. e.g., http://abc.com, https://abc.com, http://abc.com:81 belong to different origins as they differ in one of scheme, host or port. Remember that http://abc.om/user1 and http://abc.om/user2 are different URLs but not different origins. Also, a domain (http://abc.com) and its subdomain (http://sub.abc.com) belong to different origins (this particular restriction can be relaxed using a technique called domain relaxation, which is out of scope of this topic).

Can my client script read your emails?

Browsers restrict JavaScript calls to server (read as AJAX) based on Origin. This is governed by a policy called Same Origin Policy. In other words, client script in your page can make calls only to your server (strictly speaking, origin). If this rule wasn’t there, it would have been possible to write a script in some arbitrary web page which can read your web based email conversations.

Cross Origin AJAX? Really?

For the reason stated above, a page can make an AJAX call to the same origin from which it originated. If I am allowed to coin an acronym stressing on the boundaries of AJAX, I would coin “AJAX-FOO”, which expands to “Asynchronous JavaScript And XML For Own Origin”. As soon as a new XMLHttpRequest is fired to a remote origin, browsers check the origin of the page with the destination of the request. If both are same, the call is allowed. Else, the call is blocked with an appropriate error message. So there is nothing like Cross Origin AJAX.

Understanding JSONP (TL;DR: It’s all about script tag hack!)

As they say, necessity is the mother of invention. When web2.0 APIs were introduced, they desperately wanted cross origin interactions. JSNOP was discovered as a hack/work-around to bypass the restrictions of Same Origin Policy.

The idea behind it is very simple. Same Origin Policy doesn't apply for scripts (and a couple of other elements too). A <script> tag in a web page can load JavaScript from any origin (i.e., when you embed jQuery.js pointing to a CDN, loading from a remote origin is allowed). Using this loophole, one can create cross origin requests.

Simple example to create your own JSONP service

1. Create a HTML page having two JavaScript files. In script1.js, create a function “processData”

function processData(data){ 
console.log('Hello '+data.firstName+' '+data.lastName);
}

2. In script2.js execute the above function by passing valid JSON data:

processData({firstName:'Krishna', lastName:'Chaitanya'}); 

3. When you load the page, both the script files load, code in the second file executes the function defined in the first file. This is an expected behavior.

4. Create a file “service.abc” (yes, create it with this dummy extension. This is going to be your web service) and place it in the same folder. Open it and write the same code as in step 2. Now open your web server (IIS or your preferable one), go to your site, open mime types section and add a new mime type “.abc” having the mime type value “text/javascript”.

5. Now remove reference to “script2.js” and add a reference to this new file “service.abc” in the head section like this:

<script type="text/javascript" src="service.abc"></script>

6. When you load the page now, you get the same behavior as that of script2. So far, everything is in the same origin. Place the file “service.abc” in another origin (simply create another website on a different port number-recollect that different ports means different origins) and reference it in script tag and the code still works.

What you have done is, you have loaded content from a remote service via script tag injection. This is the essence of JSONP. The idea of having a random file format “.abc” is just to show that any file which can serve script content will hold good for this. You may use your “.aspx”, “.asmx”, “.ashx” or whatever to achieve this.

Hence, JSONP is always a script Injection and has nothing to do with XMLHttpRequest object and AJAX.

How JavaScript libraries like jQuery help (mislead) you

If you use libraries like jQuery, they give you a common syntax which works for AJAX as well as JSONP hack. They do a lot of work behind the screens to make a JSONP script injection

$.getJSON('http://graph.facebook.com/zuck?callback=?', function (data) { 
console.log(data);
}

In the above API for Facebook, if the value for callback is given by the developer as “https://graph.facebook.com/zuck?callback=fetch”, facebook returns json data by wrapping it in the function “fetch” (open the link in your browser and check the output. Note: IE will ask to save the response as ".js" file.). If the function name is omitted, jQuery handles it in an interesting/tricky manner. It takes the success callback as the function to be executed (similar to “processData” function as declared above), creates a random function name and assigns the callback to it. The server too responds by wrapping its json data in the random function name which it got from the request (see the first screenshot in this blog post). Once the http transaction is done, jQuery destroys the random function.

(Note: To test the trick jQuery uses, I used burp proxy to intercept and pause the request sent by jQuery. While pausing, I typed jQuery’s random function name in browser’s console and it printed the definition of the function. After the response is received, I did the same and I got that function is undefined. This way I was able to deduce the trick jQuery uses for JSONP).

In this process, jQuery does not fire an AJAX call. All it does is injection of script tag and serving javascript in its response. Since the syntax for AJAX and JSONP are maintained the same, web developers tend to confuse about JSONP.

So what mime-type should be served for a successful JSONP request?

Well, this is a topic of confusion, at least for me. Since the served content is JavaScript, the preferred mime-type should be “application/javascript” or “text/javascript” or may be "application/json". In my demo, I’ve changed the mime type of the above service to “image/gif”, “text/css” etc and the script still worked in all modern browsers without any warnings. Also, there are cases where browsers show a "save file" dialog when wrong mime type is served. Enabling adhoc mime types has security concerns and research is being done in this area for standardizing mime-type. At least for now, “application/javascript” can be used and anyways CORS is the future, so no more content type worries.

Hope the article provided useful info. Share your thoughts or discuss if you see the need for any corrections. Happy coding Smile