A Complete Guide to CORS (Cross-Origin Resource Sharing) for REST APIs

Many current web applications rely on near-frictionless and simultaneous access to numerous API providers' Web APIs. However, the web's default is to prohibit such "loose" behaviour, much like a firewall that blocks access to untrusted parties in the name of security.

That default, thankfully, can be safely altered. Before doing so, however, it is necessary for both Web application developers and API providers to understand the concepts of Cross-Origin Resource Sharing (CORS).

We will cover the following:

  1. History
  2. What is CORS?
  3. Origin Server and Cross-Origin Server
  4. Type of CORS Requests
  5. How Does CORS work?
  6. How the CORS Policy is Implemented by Browsers
  7. Failures of CORS
  8. Vulnerabilities Resulting from CORS Configuration Errors
  9. Avoiding CORS Misconfiguration Security Vulnerabilities

History

As soon as browsers allowed the inclusion of scripts in web pages in the early days of the internet, developers began placing third-party scripts on them. Scripts, as we all know, may be used for a variety of things, from simple animations to advertisements, tracking, and social network integration.

Because these third-party scripts can access user information on the front-end or back-end, this type poses a security risk. A script, for example, may attempt to steal cookies, local storage, or even exploit this information to attack sensitive applications such as banks, email clients, or even local network devices on behalf of the user.

Today, it's considerably riskier because most websites embed/call dozens of third-party scripts.

The Same Origin Policy was born as a result of this. The browser would only allow a web page to access resources from other pages with the same "origin."

If two URLs have the same host, port, and protocol, they are said to have the same origin.

It was required to give a solution for scenarios when one needed to "cross" origins on a webpage as soon as the same-origin policy was enforced. CORS (Cross-Origin Resource Sharing) is the name of this approach.

CORS is a protocol built on top of HTTP that allows the backend to instruct the browser to allow front-back interactions. A preflight request is sent by the browser before each non-simple request is made. Every server response (preflight or not) should then include a set of headers that allow a subset of otherwise banned interactions.

This strategy appears to be difficult, and it is. As a result, by default, a set of cross-origin interactions is allowed. Simple requests are accepted by default and do not require CORS, according to the fundamental rule. CORS is thus an opt-in protocol. As a result, most websites will not have to deal with it until they require it.

As is customary in the front-end world, defining simple is difficult because the exact meaning varies from browser to browser. We can say that a simple request employs the HEAD, GET, or POST methods as a rule of thumb. Only a small number of headers and content types are permitted.

What is CORS?

CORS is an HTTP header-based method that allows a server to specify any origins (domain, scheme, or port) other than its own from which a browser should allow resources to be loaded. It also employs a method in which browsers send a "preflight" request to the server hosting the cross-origin resource to ensure that the real request is permitted. The browser transmits headers indicating the HTTP method and headers that will be used in the real request during this preflight.

For example, a cross-origin request is when the front-end JavaScript code from https://domain-a.com makes a request to https://domain-b.com/data.json using XMLHttpRequest.

Browsers restrict cross-origin HTTP requests launched by scripts for security concerns. XMLHttpRequest and the Fetch API both adhere to the same-origin policy.  This means that unless other origins' responses provide the proper CORS headers, a web application using those APIs can only request resources from the same origin as the one from which the application was loaded.

Secure cross-origin requests and data transfers between browsers and servers are made possible via the CORS mechanism. To limit the hazards of cross-origin HTTP requests, modern browsers use CORS in APIs like XMLHttpRequest and Fetch.

Origin Server and Cross-Origin Server

CORS does not use the terms origin server or cross-origin server. However, these names will be used to refer to the server that hosts the source application as well as the server to which the browser will submit the CORS request.

When a user fills in a URL, the following processes occur:

  • The request is sent to a server in the domain www.example.com by the browser. This server, which hosts the index.html page, will be referred to as the "Origin server."
  • As a response to the browser, the origin server sends the page index.html.
  • Other resources are also hosted on the origin server.
  • The browser can also get resources from a different domain, such as www.xyz.com. This server will be known as a "Cross-Origin server."
  • To load content on the screen without refreshing the page, the browser uses Ajax technology using the built-in XMLHttpRequest object, or since 2017, the new fetch function within JavaScript.

The following steps are depicted in the sequence diagram:

The origin server is the server that retrieves the web page, while the cross-origin server is any server that is not the origin server.

Type of CORS Requests

Depending on the operations we want to do with the resource in the cross-origin server, the browser determines the type of request to send to the cross-origin server.

There are three types of requests that the browser can make to the cross-origin server:

  1. Simple
  2. Preflight
  3. Requests with credentials

By running an example in the following sections, we'll be able to understand these request kinds and monitor them in the browser's network log.

#1 Simple CORS Requests (GET, POST, and HEAD)

The browser sends simple requests to accomplish activities it considers safe, such as a GET request to fetch data or a HEAD request to check status. If one of the following circumstances exists, the browser's request is simple:

  • GET, POST, or HEAD are the three HTTP request methods
  • Accept, Accept-Language, Content-Language, and Content-Type are all CORS safe-listed headers in the HTTP request
  • When the Content-Type header is present in an HTTP request, it contains the following values: text/plain, multipart/form-data, or application/x-www-form-urlencoded
  • On any XMLHttpRequestUpload object, no event listeners are registered
  • In the request, there is no ReadableStream object

After adding the Origin header, the browser sends the basic request as a normal request, similar to the Same Origin request, and the browser checks the Access-Control-Allow-Origin header when the response is received.

Only if the value of the Access-Control-Allow-Origin header matches the value of the Origin header given in the request can the browser read and render the response. The Origin header contains the request's source origin.

#2 Preflight Requests

The browser sends preflight requests for actions that seek to change anything in the cross-origin server, such as an HTTP PUT method to update a resource or an HTTP DELETE method to delete a resource, in contrast to simple requests.

Since these queries are not considered safe, the web browser sends a preflight request to ensure that cross-origin communication is permitted before making the actual request to the cross-origin server. This category also includes requests that do not meet the criteria for a simple request.

The preflight request is an HTTP OPTIONS method that the browser sends to the cross-origin server to see if the cross-origin server will allow the real request. The browser sends the following headers along with the preflight request:

  1. Access-Control-Request-Method
    This header specifies the HTTP method to be utilised when making the actual request.
  2. Access-Control-Request-Headers
    This is a list of headers, including any custom headers, that will be delivered with the request.
  3. Origin
    The origin header, like the plain request, contains the request's source origin.

The actual request to the cross-origin server will not be sent if the OPTIONS method responds that the request cannot be made.

Following the completion of the preflight request, the real PUT method with CORS headers is delivered.

#3 CORS Requests with Credentials

In most real-life scenarios, requests sent to the cross-origin server must include some form of authentication, such as an Authorization header or cookies. The default behaviour of CORS requests is that they are sent without any of these credentials.

The browser will not allow access to the response unless the cross-origin server sends a CORS header Access-Control-Allow-Credentials with a value of true when credentials are given with the request to the cross-origin server.

How Does CORS Work?

Simple requests and preflighted requests are the two main types of CORS requests. The criteria for determining whether or not a request gets preflighted will be detailed later.

#1 Simple Requests

It does not require a preflight request (preliminary checks) before being sent.

i) AJAX request is initiated when a browser tab is opened to https://www.domain.com and GET the widgets from https://api.domain.com/widgets

ii) For cross-origin requests, the browser automatically inserts the Origin Request Header in addition to headers like Host:

GET /widgets/ HTTP/1.1
Host: api.domain.com
Origin: https://www.domain.com

[Rest of request...]

iii) The Origin request header is checked by the server. The Access-Control-Allow-Origin is set to the value in the request header Origin if the Origin value is allowed.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.domain.com
Content-Type: application/json

[Rest of response...]

iv) The browser checks the Access-Control-Allow-Origin header to determine if it matches the tab's origin when it receives the response. If this is not the case, the response will be blocked. The check succeeds if the Access-Control-Allow-Origin matches the single-origin exactly or contains the wildcard * operator, as in this example.

#2 Preflighted Requests

This is an OPTIONS request to the same URL as the preflight request.

Typical scenarios that require preflighted requests include:

  1. The Content-Type header is application/json when a website uses AJAX to POST JSON data to a REST API
  2. A website makes an AJAX call to an API, which utilises a token in the request header such as Authorization to authenticate the API

As a result, it's not uncommon for a REST API powering a single-page application to preflight the bulk of AJAX requests.

Example:

i) An authenticated AJAX request POST https://api.domain.com/widgets with a JSON payload is initiated by a browser tab open to https://www.domain.com. The browser sends the OPTIONS request first (also known as the preflight request), which includes the main request's proposed Requested Method and Requested Headers:

OPTIONS /widgets/ HTTP/1.1
Host: api.domain.com
Origin: https://www.domain.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type

[Rest of request...]

ii) The server responds with a list of permitted HTTP methods and headers. The browser will fail without attempting the CORS request if the original CORS request is intended to send a header or HTTP method that isn't on the list.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.domain.com
Access-Control-Allow-Methods: POST, GET, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Content-Type: application/json

[Rest of response...]

iii) The browser makes the original CORS request because the headers and method pass the inspection. The Origin header is also present in this request.

POST /widgets/ HTTP/1.1
Host: api.domain.com
Authorization: 1234567
Content-Type: application/json
Origin: https://www.domain.com

[Rest of request...]

iv) Checks pass and control is returned to the browser tab because the response has the correct origin in the Access-Control-Allow-Origin header.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.domain.com
Content-Type: application/json

[Rest of response...]

How the CORS Policy is Implemented by Browsers

Browsers are the only ones who enforce the CORS protocol. This is accomplished by the browser sending a set of CORS headers to the cross-origin server, which response with appropriate header values. The browser either allows access to the response or denies it by displaying a CORS error in the browser console based on the header values returned in the response from the cross-origin server.

When a web page requests a resource, the browser determines if the request is for the origin server or the cross-origin server, and if the request is for the cross-origin server, the CORS policy is applied.

With the request to the cross-origin server, the browser sends a header named Origin. This request is processed by the cross-origin server, which response with an Access-Control-Allow-Origin header.

The browser compares the value of the Access-Control-Allow-Origin header in the response to the Origin header supplied in the request and presents the response only if the value of the Access-Control-Allow-Origin header is the same.

To reflect a partial match with the value of the Origin header received in the request, the cross-origin server can utilise wild cards like * as the value of the Access-Control-Allow-Origin header.

Failures of CORS

An attacker could use cues from the error message to customise subsequent attacks to maximise their odds of success. CORS failures result in errors, however, the browser is not given details about the error for security reasons.

The only way to learn about the error is to seek details in the browser's console, which are usually in the following format:

Example 1: Different domains:

Access to XMLHttpRequest at 'https://api.domain.com/customers'
from origin 'https://www.another-domain.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin'
header is present on the requested resource.


Example 2: Different subdomain in same domain:

Access to XMLHttpRequest at 'https://api.domain.com/customers'
from origin 'https://app.domain.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin'
header is present on the requested resource.

An error "reason" message appears with the error in the browser console. Depending on the implementation, the reason message may differ between browsers.

Vulnerabilities Resulting from CORS Configuration Errors

Misconfiguration of the CORS protocol on the cross-origin server has the potential to generate security vulnerabilities in communications using the protocol. Some misconfigurations allow malicious domains to access API endpoints, while others allow untrusted sources to send credentials, such as cookies, to the cross-origin server, allowing them to access sensitive data.

Let's look at two examples of CORS vulnerabilities that might be triggered by code misconfiguration:

#1 Origin Reflection - Copying the Origin Header's Value in the Response

When a browser sends a request to a cross-origin server, it includes an Origin header with the value of the domain from which the request originated. An Access-Control-Allow-Origin header with the value of the Origin header obtained in the request must be returned by the cross-origin server.

It's possible that many domains will need access to the cross-origin server's resources. In that instance, the cross-origin server may dynamically set the value of the Access-Control-Allow-Origin header to the domain value received in the Origin header.

const express = require('express');
const app = express();

app.get('/customers', (req, res) => {
    console.log('Customers:');

    // set to the value received in Origin header
    res.header("Access-Control-Allow-Origin", req.header('Origin'));

    const customers = await Customers.find({}).exec();

    res.send(customers);
});

In this case, we're reading the value of the Origin header from the request and replacing it with the value of the Access-Control-Allow-Origin header from the response.

Any domain, even malicious ones, will be able to submit requests to the cross-origin server if this is done.

#2 Lenient Regular Expression

We may check the value of the Origin header in the cross-origin server code using a regular expression. The code will look like this if we want all subdomains to be able to submit requests to the cross-origin server:

const express = require('express');
const app = express();
...
...
app.get('/customers', (req, res) => {

    console.log('Customers');

    origin = req.getHeader("Origin");

    // Allow requests from subdomains of mydomain.com
    let re = new RegExp("https:\/\/[a-z]+.mydomain.com")
    if re.test(origin, regex) {
        // set to the value received in Origin header
        res.header("Access-Control-Allow-Origin", origin);
    }

    const customers = await Customers.find({}).exec();

    res.send(customers);
});

Requests from sites like https://xyzmydomain.com will be served since the dot(.) character in the regular expression is not escaped. An attacker can take advantage of this vulnerability by purchasing xyzmydomain.com and hosting malicious code there.

Avoiding CORS Misconfiguration Security Vulnerabilities

Here are some best practices for implementing CORS in a secure manner:

  • We can establish a whitelist of specified domains that are allowed to access the cross-origin server in the application on the cross-origin server. When the request arrives, we should check the Origin header against the whitelist and then populate relevant values in the CORS response headers to allow or prohibit access.
  • Similarly, we should define which methods are valid for the whitelisted domains to utilise in the Access-Control-Allow-Methods header.
  • All domains that need access to resources should be validated, as should the methods that other domains are authorised to use if their access request is granted.
  • CORS scanners should also be used to discover security vulnerabilities caused by CORS misconfigurations.
  • CORS checks should be included in important application penetration testing. The OWASP CORS testing advisory gives guidelines for finding CORS-enabled endpoints and ensuring the security of the CORS setup.

Summary

CORS (Cross-Origin Resource Sharing) is a W3C definition and technique for requesting limited resources from a domain other than your current one. In other words, CORS is a method of consuming an API from a source other than your own.

CORS is a method for allowing request permissions to access a certain resource by utilising additional HTTP headers. In addition, with an HTTP OPTIONS request, some requests may trigger a preflight request exploring the server's supported HTTP methods. Setting suitable response headers and responding to preflight requests are made easier using the CORS module.

CORS was intended to allow websites to communicate with one another while also establishing the safety of certain cross-origin requests. Resources/domains could only interact with resources from their own / parent domains back then.

Images, JavaScript, web fonts, style sheets, and other items are frequently loaded from other websites/domains nowadays. CORS also gives websites more freedom and flexibility, as long as the security settings are maintained.


Monitor API Failures with Atatus

Monitoring API failures in production is very hard. You'll need tool like Atatus to figure out which APIs are failing, how often they're failing, and why they're failing.

You can see the most common HTTP failures and get detailed information about each request, as well as custom data, to figure out what's causing the failures. You may also view how API failures are broken down by HTTP Status Codes and which end-users have the most impact.

Try your 14-day free trial of Atatus.

Janani
Janani works for Atatus as a Content Writer. She's devoted to assisting customers in getting the most out of application performance management (APM) tools.
India

Monitor your entire software stack

Gain end-to-end visibility of every business transaction and see how each layer of your software stack affects your customer experience.