Let's talk about CORS

Cross Origin Resource Sharing, or CORS for short, is a mechanism to allow cross-origin requests (which happens when the sender and the receiver are hosted on different protocols, domains or ports), throught additional HTTP headers, between two applications with diferent origins to give access permission to specified resources.

It has a pretty deep background over it, so let’s go back to where we started to see why such a thing should exist.

Cookie-Based Auth

It became quite common nowadays to type your credentials only once and remain authenticated until you explicitly logout on any service. We usually manage this by storing a token of user’s identity on the user’s browser, inside a cookie. And then, this cookie content is sent on every request associated with the given site.

And that’s it! Now every request has the user credentials hanging on it, therefore excluding the need of asking for the credentials throughout the cookie’s lifetime. Even AJAX request may also include these credentials… and that’s something that we should be aware of.

Cross-Site Request Forgery (CSRF)

Back then, the possibilty of sending these credentials via AJAX created a vast amount of devious possibilities. Let’s create a hipotetical situation.

What if you logged into your bank website, which has cookie-based auth. After logging in, you would get a token with your credentials stored on your browser. You did some transactions and then went back to reading some interesting article on some random blog and you felt like participating on some random discussion on the comments section.

What you didn’t know is that the “Comment” button, that would submit your comment, will fire a AJAX to all possible bank websites (your bank included) and say: “as the current logged in user, transfer 200$ to account XXX”.

Sick, right? Such exploit is called cross-site request forgery and, back then, in some specific cases, it would not only work but it would make all these requests appear to be done by the logged user, since the attacker used his credentials.

Nasty, isn’t it? But fear not! People at Netscape, back in 1995, thought about that and created what we call Same-Origin Policy.

Same-Origin Policy

This policy, implemented by most of the modern browsers in different ways, restrict the interaction of a document with resources from different origins. This is critical to isolate malicious scripts.

As I said at the introduction, a given document has the same origin of a given resource if they share the same protocol, domain and port. If any of these parameters diverge we have a cross-origin request, which is restricted by this policy.

Now, with that, the malicious AJAX request from our hipotetical blog, which is hosted on a different origin from our hipotetical bank, would trigger an error.

As I said before the Same-Origin Policy implementation has some variations on different browers, but, essentially, it controlls the interaction between two origins and categorize as:

  • Cross-origin writes (tipically allowed)
  • Cross-origin embedding (such as <img>, <link> or <script> and some other tags… also tipically allowed)
  • or Cross-origin reads (tipically not allowed - even though you can read the embedded resource information).

This allowance on Cross-origin embedding is crucial because websites composition rely on external resources and assets. But after a while, applications started to rely more and more on external APIs. These requests wouldn’t pass through the Same-origin Policy due to it’s cross-origin nature. So people started to do some workaround. Using the Cross-origin embedding as a vehicle to bypass the Same-origin Policy people used the mechanism called JSON with padding, or JSONP for short.

JSONP

Essentially JSONP took advantage of the fact that the HTML <script> tag is allowed to execute the content retrieved from a cross-origin request (categorized as embedding, which explains why it is allowed). How so? Let’s go by example!

Let’s say our app is hosted on http://app.example.com and we have an endpoint http://api.example.com/user that returns the following JSON data:

{
  "name": "Party Parrot",
  "quote": "Party or die!"
}

Notice that a request from app to endpoint should clearly be classified as cross-origin due to discrepant host. So, using the knowledge up until now we know that the Same-origin Policy wouldn’t allow us to make an AJAX request to endpoint so we need a workaround.

What if we do that?

<script src="http://api.example.com/user"></script>

As the usual behavior of the <script> tag, the browser would request the content of the src, download it and evaluate it’s content. When trying to evaluates our JSON it will either interpret it as a block and fire a syntax error or as Object literal. In either way, we don’t have direct access to its content in a way that it can be worked with.

To get around this problem, we can use the JSONP method. With this, the server wraps the JSON content with JavaScript code. Usually, it wraps the data with a function call, with the name of the function, by convention, provided as a named query parameter. Like that:

<script src="http://api.example.com/user?callback=doSomething"></script>

Will return:

doSomething({ "name": "Party Parrot", "quote": "Party or die!" });

And, as always, the script tag will evaluate this code. So… to complete this, all we have to do is to declare our handler beforehand:

<script>
function doSomething(data) {
  console.log("Our parrot data", data);
}
</script>
<script src="http://api.example.com/user?callback=doSomething"></script>

And there you go! We just made a cross-origin request bypassing the Same-origin Policy. But, doing things this way, as you can probably imagine (I hope), there are some serious security implications (which I will not cover here). For this reason, people from W3C proposed a protocol called Cross-Origin Resource Sharing, or CORS for short.

Cross-Origin Resource Sharing

CORS consists in a set of additional headers to indicate whether the content response can be shared or not. To illustrate this, consider that all the requests mentioned are AJAX (XMLHttpRequest).

With CORS, the server classify the request in two cases: Simple and Preflighted requests.

Requests are classified as Simple requests when it is supported by a HTML form: a GET, HEAD or POST. The latter applies only when content type is either text/plain, application/x-www-form-urlencoded or multipart/form-data.

Now, requests that cannot be classified as Simple are classified as Preflighted requests. These requests trigger a CORS-preflight request, that checks if the CORS protocol is understood by the server. This preflight request uses OPTIONS as method.

In summary, the basic mechanism of CORS is to include the following headers which indicates what is being requested:

  • Origin: Which contains the request origin
  • Access-Control-Request-Method: Which indicate the future CORS request method
  • Access-Control-Request-Headers: Which indicate the future CORS request headers

And then include the following headers in the response which indicates what is allowed:

  • Access-Control-Allow-Origin: Which indicates which domains can access the response content
  • Access-Control-Allow-Credentials: Which indicates whether or not include the browser’s cookies containing user credentials
  • Access-Control-Allow-Methods: Which indicates which methods are supported
  • Access-Control-Allow-Headers: Which indicates which headers are supported
  • Access-Control-Expose-Headers: Which indicates which headers can be exposed as part of the response
  • Access-Control-Max-Age: Which indicates how long the Methods and Headers can be cached

So, if we send this example of a Simple Request from a fictional origin http://parrot.com

GET /party HTTP/1.1
Host: api.parrot.com
Origin: http://parrot.com
...

…the respose will be sent to the user whether it has the CORS specific headers or not. It should be something like this:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-type: application/json
...

(response json here)

Simple, right? Cool…

Now, when sending a POST to a different host configures a Preflighted request, that triggers an OPTIONS preflight request like this:

OPTIONS /party HTTP/1.1
Host: api.parrot.com
Origin: http://parrot.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,accept
...

And the preflighted respose should be:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://parrot.com
Access-Control-Allow-Methods: POST, GET, PUT
Access-Control-Allow-Headers: content-type,accept
...

And, since the OPTIONS preflight succeeded, the browser will then send the actual request:

POST /party HTTP/1.1
Host: api.parrot.com
Origin: http://parrot.com
...

(request JSON here)

And the corresponding response will be:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
...

And that’s pretty much how it works. The implementation is a little different from each browser, but they generally operate in a similar way. For example, some browsers add these additional headers to Simple CORS request and others don’t. There’s even a Firefox issue related to this subject.

Sources: