- Are using javascript on the client side.
- Need to integrate with services that are not completely under your control (or that reside in a different “origin”).
- 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 PolicySame-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 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.
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).
No comments:
Post a Comment