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).

Update: Apparently some firewalls strip these CORS HTTP headers by default. For more information, check out Brian’s post here. Recap: if some of your users are not able to issue cross-domain requests when using CORS, it might be due to a firewall in their network.

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). Some firewalls strip CORS headers.

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.

Standard

35 thoughts on “Cross-Domain requests in Javascript

  1. To mitigate security concerns for the server-side proxy, I’d suggest passing a nonce/ID rather than a URL. The server would look up this ID and translate to the correct URL.

    For example: “GET /proxy?source=abc123”

    It doesn’t get around some authentication issues but it’s still worth a mention.

  2. VERY NICE ARTICLE….Thank you so much. I didn’t know that the browser was responsible for the SOP….I thought it was the web service’s servers doing it. SO this has cleared it up so much for me. Thank you. 🙂

  3. Rohit says:

    Excellent post! Very well explained same origin policy and cross-origin concepts. One of the cleanest posts about something technical. Thanks. 🙂

  4. Hi, I have one doubt. Is it necessary that server must support “Access-Control-Allow-Origin” header in CORS. If yes, then what will happen if server doesn’t support it ?

    • Yes, if you take the CORS approach you will only need server-side changes to return extra CORS HTTP headers. If you don’t have access to the server you will not be able to take the CORS approach and should look into the other alternatives (JSON-P or server-side proxy).

      • Thanks JVANEYCK for your reply.

        So according to you, if my server is not supporting CORS HTTP headers, then JSONP & Server side proxy are the other alternatives.

        Correct ?

        Correct!

  5. sally k says:

    This was a really long article with a lot of explanation, but can you please just give us some code that would show us some examples of how to deal with the problem?

  6. Great article! Thanks for sharing your work. I agree with the intent from W3C but I a pain in the back for legit scenarios. I’m trying to build a service that will extract information for the users automatically but I’m being blocked of using a client side get because of CORS and I’m being blocked of using a server side get because the sites are actively blocking server requests…

  7. I must thank you for the efforts you’ve put in penning this website.
    I really hope to view the same high-grade blog posts from
    you later on as well. In fact, your creative writing abilities has encouraged me to get my very own blog now 😉

  8. Jack Sutton says:

    In reference to “Execute run_servers.bat in one of the subfolders…”, How do I execute the .bat file? I’ve tried npm start and npm run

    • Hi Jack,,those are Windows batch files.
      If you want to run the examples on a different OS you can just start the client and server yourself manually using for both client and server of a specific example. If you look at the contents of the .bat file you can see that I just spin them up by running “node respondingServer” and “node requestingServer” for example.

      Hope this helps!

  9. Great article!
    Quick question:
    Can one use the CORS server to “pass through” (for example) a google page so that the client will allow it to be displayed in an iframe….?

    This is a still a “problem” that is baffling me since browsers started to enforce these strict cross-domain security rules…

    Any ideas on how to to do that?

    Thanks
    Gerard

    • That’s true!
      Then again, you could send (and parse results from) these requests with Postman, curl, a node.js server-side script or anything else really, as long as it’s not javascript running in the browser. That’s the use case where the Same-Origin-Policy applies and you need to set up CORS or something similar to explicitly allow them.

  10. James Watkins-Harvey says:

    Regarding the fact that some corporate firewalls / proxies strip CORS headers, a very simple solution is to systematically use HTTPS communications for cross-domain requests. Client-side proxies can’t inspect nor alter SSL exchanges, so they can’t filter out CORS headers. Given that real SSL certificates can now be obtained for free (ie. Let’s encrypt and others), there should be no reason not to take this strategy for cross-domain requests, at least in production context.

Leave a reply to jvaneyck Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.