Cross Origin Resource Sharing

Cross Origin Resource Sharing

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, having those sites make requests to other sites that the user uses and already has authentication cookies for, and reading the response. 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 scripts loaded via script tags are considered part of the same page (DOM) the restrictions of the Same Origin Policy don't present a problem. 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 make 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 in a way that your application can read the data.

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 without a preflight request, which we'll cover in the next section.

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 and you shouldn't depend on it to be one. The Same Origin Policy only protects users who are using browsers that have implemented it. If you use a non-standard browsing application 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 our actual GET request. 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.

Can't I just make a server-side proxy?

When first learning about the Same Origin Policy and CORS many developers immediately wonder why they can't just make a proxy script on their own server. Then their client-side code can just call the proxy and the proxy can talk to the cross-origin domains for them. There is one big difference between talking to a cross-origin domain from the browser and talking to a cross-origin domain through a server-side proxy. Cookies.

Let's say for example that you open your browser and navigate to yourbank.com and log in. Your browser will store an authentication cookie for yourbank.com so that you don't have to log in every time you refresh or browse to a new page while viewing your accounts. Any request the browser now makes to yourbank.com will pass along this authentication cookie with the request, including ajax requests.

Now let's say you open a second browser tab and browse to the ficticious site attacker.com and that site has client-side code that tries to make requests to yourbank.com/account. Pretending for a moment that there is no Same Origin Policy implemented in the browser, this requst would go through and the browser would pass along the authentication cookie you just refreshed in the other tab. That would be a bad thing as it is now possible for attacker.com to see the response from yourbank.com and can pretend to be you when making requests to it. Thankfully, the Same Origin Policy helps mitigate this issue for most people browsing the web with a standard web browser.

"Aha!" says the malicious developer of attacker.com, "I'll just create a server-side proxy and then the Same Origin Policy will no longer apply. Muahahahaha!" So he creates the proxy and changes his client-side code to make requests to something like attacker.com?url=yourbank.com/account. Sure, attacker.com is now definitely able to talk to yourbank.com without any restrictions, but what the developer of attacker.com didn't realize was that he couldn't grab any cookies for yourbank.com when he made the client-side request to attacker.com?url=yourbank.com/account. When his server makes the request to yourbank.com it will not contain any authentication cookies from the user's browser, preventing attacker.com from making requests to yourbank.com on the user's behalf.

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 or the Same Origin Policy. 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.