Cross Origin Resource Sharing

Datetime:2016-08-22 23:49:42          Topic: JSONP           Share

I've spent way more time reading about CORS than I probably should have, but pretty much everything I read only confused me further. CORS is actually a pretty simple concept once you understand it so I decided to put this post together to explain it in the simplest terms I can. In addition to hopefully making it easier for others, I hope to solidify the information in my own head.

Three or four years ago I remember running into the Same Origin Policy . I tried finding an image of what the old error used to look like in the Chrome Javascript console but I can't find one. I just know that it would bark at me about cross-domain requests and then I would get upset. The reason for the restriction was to prevent the user from browsing to malicious sites and having those sites make requests to other sites that the user uses and already has authentication cookies for. For instance, if you went to attacker.com , that site could not make a cross-domain call to yourbank.com/youraccount , passing in the authentication cookies stored in your browser for yourbank.com .

The workaround was to use a hack called JSONP .

JSONP

JSONP involved inserting a script tag into the page with the source URL pointed to the cross-domain resource you were trying to talk to. That URL would also contain a query string parameter named callback and its value would be the name of a global function on your page somewhere. Since script tags are not restricted by the Same Origin Policy the request would be allowed. Here is an example:

<pre id="response">
</pre>
<script>
    function myFunction (data) {
        document.getElementById('response').innerHTML = JSON.stringify(data);
    }
</script>
<script src="https://api.github.com?callback=myFunction">
</script>

See the Pen beJGE by Alex Ford ( @Chevex ) on CodePen .

For JSONP to work, the server must wrap the data it was about to return in a function call, using the function name you passed through the callback parameter. So instead of getting a simple object back like this:

{ property: 'value' }

You'd actually get back a javascript function call that passes in the data like this:

myFunction({ property: 'value' });  

The call would then be inserted into the script tag on your page. Since browsers run script tags as soon as they are inserted into the page, the call to myFunction would run and the data would be passed to the function where you could do what you needed with it.

JSONP is a pretty simple hack but it's ugly, requires a global function to call, can only send GET requests (it is just a script tag after all), and it's very difficult to manage errors with the request. Most popular frameworks/libraries such as JQuery made it really easy to do JSONP by doing all the script tag stuff for you behind the scenes. Still though, JSONP was definitely not an ideal way to make cross-origin requests.

CORS is just a specification to standardize cross-origin communication in a way that allows both the browser and the server to opt-in. If you think about it, the JSONP hack did exactly that because the server had to know how to form the response for a JSONP request. If the server didn't wrap the returned data in your function call then the response would not be valid Javascript and would fail.

CORS

One of the first things to realize about CORS is that you don't need to do anything in your client-side code in order to use it. CORS is implemented by the browser itself. Instead of barking at you about the Same Origin Policy when you try to make a cross-origin request, it will now try to establish the request one of two ways.

If the request was made using GET , POST , or HEAD methods and contains only headers from the approved list of headers below, then the request will simply be allowed to go through.

Allowed request headers:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

The browser will add a header called Origin that is set to the origin URL, to the request. The server must then reply with a response header called Access-Control-Allow-Origin and it must be set to the same value of the origin request header or * , indicating that any origin URL is fine. The request would look something like this:

Notice that we made one request to api.github.com . That request included the Origin header and Github responded with the Access-Control-Allow-Origin header with a value of * , telling the browser that we are good to go. If the response came back without the proper header then the browser would not allow us to see the response that came back from the server even though the request was indeed made. You can see this easily when you use a web debugging proxy like Fiddler or Charles . I made an ajax request directly to codetunnel.com which is not an API and therefore is not setup to return CORS headers; the browser should prevent us from seeing the response on that request.

As you can see the request was indeed "canceled". Or was it? The only way the browser could know that the headers weren't returned is to have made the request in the first place. Let's take a look at Charles, which I had running when I loaded the page that made the request.

Well how about that? The request was made and a response did come from the server. The browser simply did not let my little web application see the response, which would be good if my little app was trying to view my bank accounts. That said, the Same Origin Policy is hardly a tight form of security; as you can already see, there are plenty of ways around it. CORS only protects users who are using browsers that have implemented it. If you use a non-standard browser then it's very possible for a malicious website to reach outside of its origin domain and talk to other resources on the web on your behalf.

Preflight Requests

If the request uses other HTTP verbs like PUT or DELETE , or adds headers that are not specified above, then the browser will send a preflight request before the actual request to make sure the server is cool with receiving the actual request. The reason for this is that servers are used to receiving requests that conform to the above criteria so no preflight request is required to see if the server is okay with receiving the actual request. The only thing the browser cares about in that instance is whether or not the server returns the Access-Control-Allow-Origin header.

If the browser sends out a preflight request, that request will use an HTTP verb called OPTIONS . This request is just so that the server knows what's about to come and can decide if it should allow it or not. If the server does not respond to the preflight request with a succcess (200) code or it does not include the right headers then the browser will not even attempt to make the real request.

If we add a custom header to our ajax request then you will see the browser automatically include the preflight request before the real request. Here is an example of the same request we made earlier, but with a customHeader header included:

Only one request was made to api.github.com and that request is the preflight request. We included a custom header called customHeader which you can see listed in the Access-Control-Request-Headers request header. Notice that the response header Access-Control-Allow-Headers does not list our customHeader . This is why the real request was never made; Github did not respond to the preflight request saying that our custom header is allowed so the browser did not try to make the real request. Now let's make the same request with a custom header that Github does allow. We'll use the last one listed in the Access-Control-Allow-Headers response header, X-Requested-With :

This time we can see that both requests were made. First the OPTIONS request was made, which is the preflight request. The browser confirmed that Github was okay with our request and went forward with it. Here are the details of our actual request:

You can see that our actual request contained our custom X-Requested-With header, which I set to "AngularJS" myself :)

As time marches forward these preflight requests may become a thing of the past, but for now the specification authors felt that there were enough servers that may not know what to do with a DELETE request for instance, that they felt the preflight was necessary. The preflight request was the part that baffled me the most when I was first learning about CORS. It wasn't until I realized it was there for backward compatibility purposes that it started to make sense.

It's Just A Specification

It's important to keep in mind that CORS is just a specification. It provides an extremely superficial layer of security. You still need to provide proper security measures in your application and on your server. A malicious hacker can make any requests they desire to make using other applications that don't adhere to the CORS specification. CORS is just a convenience to help mitigate some of the issues prevalent on the web, nothing more. If you're maintaing a server-side API, do not depend on CORS to protect your server from unauthorized requests.





About List