DEV Community

loading...
Cover image for Understanding CORS

Understanding CORS

Anvil Engineering
Developer focused company, providing APIs and tools to help the world eliminate paperwork.
・6 min read

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 for full
HTML, CSS and JS files, and this GitHub Gist for the backend.

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()
}
Enter fullscreen mode Exit fullscreen mode

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?

$ 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"}]
Enter fullscreen mode Exit fullscreen mode

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

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.
INFO: 127.0.0.1:54748 - "OPTIONS /items HTTP/1.1" 405 Method Not Allowed
Enter fullscreen mode Exit fullscreen mode

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

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”.
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").

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

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:

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

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']
)
Enter fullscreen mode Exit fullscreen mode

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

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.

Discussion (3)

Collapse
roshanraj profile image
Roshan Raj

Great article.
But I have a question, does doing this mean I will not be able to hit those API from Postman anymore?
Since the backend is now only accepting request from the specified frontend origin?

Collapse
elijahtrillionz profile image
Elijah Trillionz

Hello, from my experience if you are using the postman's website, it will be blocked by CORS policy. So postman recommends downloading their app which is not restricted by the CORS policy.
Like he has stated, it is enforced by modern WEB BROWSERS.

Collapse
elijahtrillionz profile image
Elijah Trillionz

Great article, thanks for sharing