Understanding CORS
Published: 2021-06-10 22:21:20

CORS, or _Cross-Origin Resource Sharing_, is one thing that can bite a developer
early on when creating a web app or backend service. It’s a check by modern
browsers which provides added security for the browser user. It’s important to
note that this is purely enforced by the browser, although as a whole, both
web servers and web browsers play a part.

For example, CORS can help prevent a malicious case where a website executes an
HTTP request (via the Fetch API or `XMLHttpRequest`) to a different
site/domain where a user may be logged in. Without CORS, that malicious website
can receive a fully authenticated response containing session data, cookies,
and/or other potentially (hopefully encrypted!) sensitive data.

Let’s take a look at how that would work in a world without CORS:
* A user just visited "https://mybank.example", one of the most popular
  banking websites, to complete a few transactions.
* The user, maybe on another tab, visits "http://not-suspicious.example".
* Unknown to the user, `not-suspicious.example` contains a script that
  sends requests to a list of endpoints from very popular banking sites. This
  is all done in the background.
* If a response comes back containing user session data or other sensitive
  user data, the malicious site now has the means to impersonate the user.

Now the same example, but on a browser with CORS enabled:
* A user just visited "https://mybank.example", one of the most popular
  banking websites, to complete a few transactions.
* The user, maybe on another tab, visits "http://not-suspicious.example".
* Unknown to the user, `not-suspicious.example` contains a script that
  _attempts_ to send requests to a list of endpoints.
* Before each request, however, the user’s browser sends a request known as a
  "preflight request" to check if the request is possible.
* Now, let’s assume all banks are up-to-date with security. Each API server
  responds and tells the browser that `not-suspicious.example` is _not_ an
  origin that it trusts.
* At this point, the browser considers the preflight request as failed,
  which also stops the real request from executing.

On the last three points of the CORS-enabled example, the browser has done its
job and prevented the attack. However, that also highlights one of its
weaknesses: the browser is key, but it can be easily disabled (i.e.
the `--disable-web-security` flag for Chrome and via an extension on Firefox).
CORS should be treated as another mechanism to prevent certain attacks, and cases
where it’s disabled should be considered as well. It should be only a part of a
more comprehensive solution to secure your servers and to protect your users'
data.

On the last three points of the CORS-enabled example, the browser
has done its job and prevented the attack. However, that also highlights one
of its weaknesses: the browser is key, but CORS enforcement can also be
disabled. This mechanism should be treated as another mechanism to prevent
certain attacks and should be part of a more comprehensive solution to secure
your servers and to protect your users’ data.

Now that we know what can happen without CORS, let’s step into how someone might
discover this during development and dig into how to get your app ready.

## Getting started

You have a project idea that will probably work well as a web app. You also 
want it to be modern — who wants a plain HTML site in 2021, right? That means
you’ll need Javascript. You decide on a simple architecture consisting of:

* A backend server - Node.js, Python, PHP, etc.
* A Javascript/HTML/CSS frontend maybe with a framework - React, Vue.js, Angular, etc.

Perfect. Let’s whip up a quick prototype. See [JSFiddle here](https://jsfiddle.net/allananvil/4u150obq/4/) for full
HTML, CSS and JS files, and [this GitHub Gist](https://gist.github.com/aalmazan/4b47861f1f0126fc5f9987de474abc6b) for the backend.

```javascript
const API_URL = 'http://localhost:8000'
const button = document.getElementById('do-something')

function getResultEl () {
  return document.getElementById('result')
}

function handleResponse (response) {
  try {
    response = JSON.parse(response)
  } catch (e) {
    // Something went wrong
    console.log({ error: e })
    response = null
  }

  const html = response !== null && response?.length
    // Put our data in a list
    ? response
      .map((item) => `<li>${item.name}</li>`)
      .join('')
    // Or tell us it failed
    : '<li>Could not get response</li>'

  getResultEl().innerHTML = `<ul>${html}</ul>`
}

// Make our button send a request to our backend API
button.onclick = (event) => {
  const xhr = new XMLHttpRequest()
  xhr.open('GET', `${API_URL}/items`)
  xhr.setRequestHeader('Content-Type', 'application/json')
  // Also set any custom headers if you need, such as authentication headers
  // xhr.setRequestHeader('X-My-Custom-Header', 'some-data')
  xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
      handleResponse(xhr.response)
    }
  }
  
  // Send some optional data
  xhr.send()
}
```

## Checking our work

Now that everything’s set up, let’s double-check that our endpoint works fine
when we call it from our site. What does `cURL` say?

```bash
$ curl  "localhost:8000/items" -v
> GET /items HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< date: Mon, 07 Jun 2021 21:16:05 GMT
< server: uvicorn
< content-length: 48
< content-type: application/json

[{"name":"One"},{"name":"Two"},{"name":"Three"}]
```

Looking good. Onto the browser… but it doesn’t work when you hit the button.
Why? Let’s check our browser’s Developer Tools. In this case, we’ll be using
Firefox below:

![CORS Blocked Firefox](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z348otizbxromy8m2pd8.png)

A few things just happened:
1. In our Javascript file, we send an HTTP request to our API server at `http://localhost:8000`.
2. There’s not one, but two requests that were sent and they both returned
   error responses from our server.
3. Checking our API logs we also have an error*:
    * Technically, this can be resolved by explicitly allowing and handling
      the OPTIONS HTTP verb, but will still yield the same result.

```bash
INFO: 127.0.0.1:54748 - "OPTIONS /items HTTP/1.1" 405 Method Not Allowed
```

A quick look at the request headers on the first request also shows CORS
headers (the ones that begin with “Access-Control-Request-”).

![CORS Blocked Firefox](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oj2hxihxn382g9qt1nwk.png)

That sequence of events was your browser’s CORS enforcement at work.

## So what is the browser doing?

Going back to the definition: CORS stands for [“Cross-Origin Resource Sharing”](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
As seen in the example, the browser is trying to make a request from
`localhost:63342` (the frontend) to `localhost:8000` (the backend). These two
hosts are considered different "origins" ([see MDN’s full definition for "origin"](https://developer.mozilla.org/en-US/docs/Glossary/Origin)).

Once a cross-origin request is detected, the browser sends a preflight request
before each cross-origin HTTP request to make sure the actual request can be
handled properly. This is why the first request in our example was an
`OPTIONS` request that we never called for in the Javascript code.

On Chrome’s DevTools, you can also see this happen more clearly as it combines
the request and the preflight request:

![CORS Blocked Chrome](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g0p9mohapscqy1t9m0jn.png)

## Getting your backend ready

The good news: depending on how your backend is developed, handling CORS can be
as simple as installing a package and/or changing a few configs.

As examples, in the Javascript world, koa and express both have middleware
packages that have quick setup:

* https://expressjs.com/en/resources/middleware/cors.html#installation
* https://github.com/koajs/cors

In the example here, I’ll be using a snippet from a FastAPI app as it
demonstrates the headers more succinctly:

```python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()

app.add_middleware(
    # We add the middleware here
    CORSMiddleware,
    # These are the options we give the middleware and they map easily to their
    # associated CORS headers
    allow_origins=['http://localhost:63342, ‘http://localhost’],
    allow_methods=['GET', 'POST']
)
```

Keep in mind that the same domain with a different port requires a new entry.
In the snippet above under `allow_origins`, we’ve added `localhost` and
`localhost:63342` since those are the URLs where we might call our backend API
for data.

Also under `allow_methods`, you can see that we can finely tune our backend to
only accept certain methods. You could, for example, lock down this API service
further by only accepting `GET` requests, if it’s a simple service that provides
data without requiring user input -- like an API that provides business hours
for a specified store.

With that ready, let’s try making the request again. Below is the preflight
request (`OPTIONS`):

Perfect. It’s now allowing our origin, and shows us the allowed methods. Also,
it shows which headers are allowed in the requests. The allowed headers listed
are typical defaults, but if you need to use other headers for your use-case,
you can allow all of them completely with `access-control-allow-headers: *` or
explicitly list all the headers you want to support.

For a more detailed listing of CORS-related headers, take a look at [Mozilla’s documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#the_http_response_headers)

Hopefully this brings clarity and demystifies any questions that you may have
had with CORS, its effects, and getting a simple app to support it. Having a
sound CORS policy should only be considered as a small cog in the complex
world of web security. Since this only protects one specific attack vector,
one should stay vigilant to keep their servers and users’ data secure.

If you’re developing something exciting with PDFs and/or paperwork, we’d love
to hear from you. Let us know at [developers@useanvil.com](mailto:developers@useanvil.com).