Published on

CORS (Cross-Origin Resource Sharing)

Authors

What is CORS?

CORS, or Cross-Origin Resource Sharing is a browser security mechanism that controls page access to different origin resources due to a browser's same-origin policy. CORS restricts cross-origin requests in order to prevent (potentially) harmful requests towards server. Before we proceed, let's clarify what is the 'origin' in the context of the web (source: MDN):

Web content's origin is defined by the scheme (protocol), hostname (domain), and port of the URL used to access it. Two objects have the same origin only when the scheme, hostname, and port all match.

In practice, when the browser is sending the cross-origin request, it expects the information about the allowed origins from the server. However, the browser does not expect this information for every request, but only for requests that fulfill certain complexity criteria (qualified as "to be preflighted"):

  • any method besides GET, HEAD and POST
  • contains any header besides Accept, Accept-Language, or Content-Language
  • contains any Content-Type headers besides application/x-www-form-urlencoded, multipart/form-data, or text/plain

If the complexity criteria above is fulfilled for a given request, the browser is sending something called a preflight request to determine the server's allowed origin. Preflight request is just a plain OPTIONS request sent by the browser prior to the cross-origin request (with the sufficient complexity). OPTIONS HTTP requests are used to retrieve the endpoint metadata, like allowed methods, Access-Control-Allow-Origin, Access-Control-Allow-Headers and Access-Control-Max-Age headers, etc. Once the server responds to the preflight request, browser is able to determine if the server is aware of the specified origin, methods and headers. Based on this response, the browser can either continue with the intended request, or throw a CORS error if the request doesn't fulfill these criteria.

How to handle CORS?

CORS handling is done on the server itself. It is done via proper configuration of CORS related headers and by setting the list of allowed requester origins.

Also, there are a couple of ways to avoid the CORS preflight checks:

  • simplify the request to not fulfill the complexity criteria (if possible) — in this case, browser doesn't do preflight check
  • enable caching via Access-Control-Max-Age header
  • access your backend via proxy - browser will send a request to a same-origin address of the proxy, and the proxy will forward the request to a cross-origin address
  • send the request from an iframe — similar to a proxy solution, you're moving your frontend part of the code to an iframe which is the same-origin as the backend

Example

Let's have a look at the practical example through a web application consisting of a simple pure Node.js server and a frontend that will make a request to it. The frontend and the backend will have different origins, since they will listen on different ports (5050 vs 8080).

First, we're going to initialize an empty project:

pnpm init

Then, we're going to install our only dependency - https://github.com/vercel/serve, simple static file server for Node.js:

pnpm install serve

const http = require('node:http')

const HOST = 'localhost'
const PORT = 8080

const server = http.createServer((req, res) => {
  res.statusCode = 200
  res.setHeader('Content-Type', 'text/plain')
  res.end('Hello from the server!')
})

server.listen(PORT, HOST, () => {
  console.log(`🚀 Server is running @ http://${HOST}:${PORT}`)
})

Let's start the server by running node server.js.

Frontend will be a plain HTML file that will make a call to our server immediately when the page is loaded:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta content="width=device-width, initial-scale=1.0" name="viewport" />
    <title>App</title>
  </head>
  <body>
    <h1>Welcome!</h1>
    <script src="./index.js"></script>
  </body>
</html>

where the content of index.js is the following:

const HOST = 'localhost'
const PORT = 8080

async function getData() {
  let response = await fetch(`http://${HOST}:${PORT}`)

  let text = await response.text()
  console.log(text)
}

getData()

To run our frontend, we will use serve which we previously installed:

pnpm exec serve -p 5050 app

(assuming our files are called index.html/index.js, and are placed inside the app folder)

Now, our frontend is running on http://localhost:5050. If we open this URL in the browser and open the dev tools, we can see the following:

CORS error

Since our frontend and backend are served on a different host (different port in this case), and the server is not sending any headers that inform frontend about allowed origin, the CORS error is thrown.

To handle this error, we need to specify the list of allowed request origins on our server via Access-Control-Allow-Origin header. This can be done in several ways:

  • set the specific origin you want to allow:
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5050')
  • allow any origin:
res.setHeader('Access-Control-Allow-Origin', '*')
  • logic which handles a list of allowed origins:
const allowedOrigins = [
  'http://localhost:5050',
  'http://localhost:8020',
  'http://example.com',
  'https://example.com',
]
const origin = req.headers.origin
if (allowedOrigins.includes(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin)
}

This is not the way you would build a production server and certainly not the way you would handle CORS, but the purpose of this example is to demonstrate a CORS error without going too much into unnecessary details.

If we add any of the mechanisms above to the server (and restart it), the browser will be informed which request origins are allowed for a given server and enable browser to make a request:

Successful request

Full example repository can be found at: https://github.com/azeljkovic/cors

Resources