Javascript, Web Development

Cross-Domain requests in Javascript

If you are developing a modern web-based application, chances are you:

  1. Are using javascript on the client side.
  2. Need to integrate with services that are not completely under your control (or that reside in a different “origin”).
  3. Have been confronted by this error message in your browser’s console:

XMLHttpRequest cannot load http://external.service/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://my.app' is therefore not allowed access.

Every time I need to integrate a web app with some external service or some server-side API I have no complete control over, I bump into this error. Google has not yet provided me with a concise description of the problem or an overview of alternatives to perform Cross-Domain requests, so this post will serve as a personal future reference.

Same-Origin Policy

We are seeing this error because we are violating something called the Same-Origin Policy (SOP). This is a security measure implemented in browsers to restrict interaction between documents (or scripts) that have different origins.

The origin of a page is defined by its  protocol, host and port number. For example, the origin of this page is (‘http’,’jvaneyck.wordpress.com’, 80). Resources with the same origin have full access to each other. If pages A and B share the same origin, Javascript code included on A can perform HTTP requests to B’s server, manipulate the DOM of B or even read cookies set by B. Note that the origin is defined by the source location of the webpage. To clarify: a javascript source file loaded from another domain (e.g. a jQuery referenced from a remote CDN) will run in the origin of the HTML that includes the script, not in the domain where the javascript file originated from.

For Cross-Origin HTTP requests in specific, the SOP prescribes the following general rule: Cross-Origin writes are allowed, Cross-Origin reads are not. This means that if A and C have a different origin, HTTP requests made by A will be received correctly by C (as these are “writes”), but the script residing in A will not be able to read any data -not even the response code- returned from C. This would be a Cross-Origin “read” and is blocked by the browser resulting in the error above. In other words, the SOP does not prevent attackers to write data to their origin, it only disallows them to read data from your domain (cookie, localStorage or other) or to do anything with a response received from their domain.

The SOP is a Very Good Thing™. It prevents malicious script from reading data of your domain and sending it to their servers. This means that some script kiddie will not be able to steal your cookies that easily.

Performing Cross-Domain requests

Sometimes however, you have to consciously perform Cross-Domain requests. A heads up: This will require some extra work.

Examples of legitimate Cross-Domain requests are:

  • You have to integrate with a third-party service (like a forum) that has a REST API residing in a different origin.
  • Your server-side services are hosted on different (sub)domains.
  • Your client-side logic is served from a different origin than your server-side service endpoints.

Depending on the amount of control you have over the server-side, you have multiple options to enable Cross-Domain requests. The possible solutions I will discuss are: JSONP, the use of a server-side proxy and CORS.

There are other alternatives, the most widely used being a technique using iframes and window.postMessage. I will not discuss it here, but for those interested an example can be found here.

Example code

I wrote some code to try out the different approaches discussed in this post. You can find the full code on my github. They should be easy to run locally if you want to experiment with them yourself. The examples each host 2 websites, a site in origin (‘http’, ‘localhost’, 3000) and one in (‘http’, ‘localhost’, 3001). These are different origins, so requests from 3000 to 3001 are considered Cross-Domain requests and are blocked by the browser by default.

Example of a failing Cross-Origin request

Consider the following scenario: a page with origin A want to perform a GET request to a page with origin B. This is what happens:

The browser issues this request correctly to the server:

GET / HTTP/1.1

The server returns the response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 57

{
"response": "This is data returned from the server"
}

Upon reception of the response however, the browser blocks the response from propagating further and instead raises the Same-Origin violation error as shown above. For example, if you are using jQuery, the done() callback of your GET request will never get fired and you will not be able to read the data returned from the server.

JSONP

JavaScript Object Notation with Padding (JSONP in short) is a way of performing cross-domain requests by exploiting the fact that script tags in HTML pages can load code coming from a different origin. Before we go into detail, I would like to state that it has some major issues:

  • JSONP can only be used to perform Cross-Domain GET requests.
  • The server must explicitly support JSONP requests.
  • You should have absolute trust in the server providing JSONP responses. JSONP could expose your website to a plethora of security vulnerabilities if the server is compromised.

JSONP relies on the fact that <script> tags can have sources coming from different origins. When the browser parses a <script> tag, it will GET the script content (residing on any origin) and execute it in the current page’s context. Normally, a service would return HTML or some data represented in a data format like XML or JSON. When a request is made to a JSONP-enabled server however, it returns a script block that executes a callback function the calling page has specified, supplying the actual data as an argument. In case your head just exploded, consider the following example to make things more tangible.

A page on origin 3000 wants to get some info from a resource residing in a different origin 3001. Page 3000 contains the following script tag:

<script 
    src='http://localhost:3001?callback=myCallbackFunction'>
</script>

When the browser parses this script tag, it will issue the GET request as normal:

GET /?callback=myCallbackFunction HTTP/1.1

Instead of returning raw JSON, the server returns a script block containing a function call to the function specified in the url, passing in the output data as an argument:

HTTP/1.1 200 OK
Content-Type: application/javascript

myCallbackFunction({'response': 'hello world from JSONP!'});

This script block is evaluated as soon as the browser receives it. The function call inside the script block is evaluated in the context of the current page. This page contains a definition for the callback function, which can do something with the data:

<script>
    function myCallbackFunction(data){
        $('body').text(data.response);
    }
</script>

To summarize:

  • Since JSONP works by including a script tag (be it in plain HTML or programmatically) which is fetched by a GET request, it only supports Cross-Origin HTTP GETs. If you want to use another HTTP verb (like POST, PUT or DELETE), you cannot use the JSONP approach.
  • This approach requires you to completely trust the server. The server could be compromised and return arbitrary code that will be executing in the context of your page (thus allowing access to your site’s cookies, localStorage, etc.). You could mitigate this by using frames and window.postMessage to sandbox cross-domain JSONP calls. For a concrete example on how to implement this, check out an example here.

Server-side proxy

An alternative to circumventing the Same-Origin Policy to perform Cross-Domain requests is to simply not make any Cross-Domain requests at all! If you use a proxy that resides in your domain, you can simply use this to access the external service from your back-end code and forward the results to your client code. Because the requesting code and the proxy reside in the same domain, the SOP is not violated.

This technique does not require you to alter any existing server-side code. It does require having a server-side proxy server that resides in the same domain as the Javascript code running in the browser.

For completeness, I’ll give a quick example:

Instead of performing a GET directly on http://localhost:3001, we are sending a request to a proxy server in our own 3000 domain:

GET /proxy?urlToFetch=http%3A%2F%2Flocalhost%3A3001 HTTP/1.1

The server will perform the actual GET request to the external service. Server-side code can perform “cross-origin” requests without a problem, so this call succeeds. The proxy server just pipes the result to the client:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
"response": "This is data returned from the server, proxy style!"
}

Note that this approach also has some serious drawbacks. For example, we haven’t touched upon security-related topics in this post. If the third-party service uses cookies for authentication you cannot use this approach. Cookies for the external domain are not accessible by your own JavaScript code and are not sent to your proxy server, so there is no way to provide the cookies containing the user’s credentials to the third party service.

CORS

Chances are you’re experiencing a slight queasy feeling in your stomach by now. If you feel the previous mechanisms all have that “hacky” smell, you are absolutely right. The previous approaches are all bypassing a legitimate browser security mechanism and bypassing it will always be somewhat dirty. Luckily, there exists a cleaner solution: Cross-Origin Resource Sharing (or CORS in short).

CORS provides a mechanism for servers to tell the browser it is OK for requesting domain A to read data coming from domain B. It is done by including a new Access-Control-Allow-Origin HTTP header in the response. If you remember the error message of the introduction, this is exactly what the browser is trying to tell you. When a browser receives a response from a Cross-Origin source, it will check for CORS headers. If the origin specified in the response header matches the current origin, it allows read access to the response. Otherwise, you get the nasty error message.

A concrete example:

Requesting origin 3000 makes the GET call as usual:

GET / HTTP/1.1

The server in origin 3001 checks whether this origin may access the data and augments the response with an additional Access-Control-Allow-Origin header listing the requesting origin:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Content-Type: application/json; charset=utf-8
Content-Length: 62

{
"response": "This is data returned from the CORS server"
}

When the browser receives the response it compares the requesting origin (3000) to the origin listed in the Access-Control-Allow-Origin header (also 3000). Since they match, the browser allows the response to be interpreted by code residing in the 3000 origin.

As always, there are some limitations to this approach. For example, older versions of Internet Explorer only partially support CORS. Also, for all but the simplest requests you have to double the amount of HTTP requests (see: preflighting CORS requests).

Summary

In this post I tried to illustrate what type of requests are classified as Cross-Origin and why they are blocked by browsers under the Same-Origin-Policy. Furthermore, I discussed several mechanisms to perform Cross-Origin requests. The table below summarizes these mechanisms.

Mechanism Supported HTTP verbs Server-side modifications required? Remarks
JSONP GET Yes (return script block containing function call instead of raw JSON) Requires absolute trust in the server
Proxy ALL No (but you need an extra proxy component in your origin) Back-end performs the request instead of the browser. Could prove problematic for authentication
CORS ALL Yes (return additional HTTP headers) Not supported on older versions of Internet Explorer. For “complex” requests, needs to make an extra HTTP call (preflighted requests)

As you can see, even for something as simple as Cross-Domain requests there is no silver bullet. If you have control over the server code and you do not have to support legacy browsers, I would strongly advise you to use the CORS approach. As always, evaluate the alternatives in the context of your current requirements and use the approach that best suits your needs.

About these ads
Standard

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s