Engineering

Understanding CORS

Author headshot
By Allan Almazan

An overview of CORS and how it can affect your webapp.

Back to all articles
Understanding CORS

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()
}

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"}]

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

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']
)

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.

Get a Document AI demo (from a real person)

Request a 30-minute demo and we'll be in touch soon. During the meeting our team will listen to your use case and suggest which Anvil products can help.
    Want to try Anvil first?
    Want to try Anvil first?