Authoritative guide to CORS (Cross-Origin Resource Sharing) for REST APIs

8 minute read

An in-depth guide to Cross-Origin Resource Sharing (CORS) for REST APIs, on how CORS works, and common pitfalls especially around security.

What is CORS?

CORS is a security mechanism that allows a web page from one domain or Origin to access a resource with a different domain (a cross-domain request). CORS is a relaxation of the same-origin policy implemented in modern browsers. Without features like CORS, websites are restricted to accessing resources from the same origin through what is known as same-origin policy.

Why does same-origin exist?

You, like many websites, may use cookies to keep track of authentication or session info. Those cookies are bounded to a certain domain when they are created. On every HTTP call to that domain, the browser will attach the cookies that were created for that domain. This is on every HTTP call, which could be for static images, HTML pages, or even AJAX calls.

This means when you log into https://examplebank.com, a cookie is stored for https://examplebank.com. If that bank is a single-page React app, they may have created a REST API at https://examplebank.com/api for the SPA to communicate via AJAX.

The cross-domain vulnerability

Let’s say you browse to a malicious website https://evilunicorns.com while logged into https://examplebank.com. Without same-origin policy, that hacker website could make authenticated malicious AJAX calls to https://examplebank.com/api to POST /withdraw even though the hacker website doesn’t have direct access to the bank’s cookies.

This is due to the browser behavior of automatically attaching any cookies bounded to https://examplebank.com for any HTTP calls to that domain, including AJAX calls from https://evilunicorns.com to https://examplebank.com. By restricting HTTP calls to only ones to the same origin (i.e. the browser tab’s domain), same-origin policy closes some hacker backdoors such as around Cross-Site Request Forgery (CSRF) (Although not all. Mechanisms like CSRF tokens are still necessary).

How is Origin defined?

Origin includes the combination of protocol, domain, and port. This means https://api.mydomain.com and https://mydomain.com are actually different origins and thus impacted by same-origin policy. In a similar way, http://localhost:9000 and http://localhost:8080 are also different origins. The path or query parameters are ignored when considering the origin.

Origin refers to the content who initiated the request which is usually the open browser tab, but could also be the origin of an iFrame window.

Why was CORS created?

There are legitimate reasons for a website to make cross-origin HTTP requests. Maybe a single-page app at https://mydomain.com needs to make AJAX calls to https://api.mydomain.com; or maybe https://mydomain.com incorporates some 3rd party fonts or analytics providers like Google Analytics or MixPanel. Cross-Origin Resource Sharing (CORS) enables these cross-domain requests.

How CORS works?


There are two types of CORS requests, simple requests and preflighted requests. The rules on whether a request is preflighted are discussed later.

I. Simple requests

A simple request is a CORS request that doesn’t require a preflight request (preliminary checks) before being initiated.

  1. A browser tab open to https://www.mydomain.com initiates AJAX request GET https://api.mydomain.com/widgets

  2. Along with adding headers like Host, the browser automatically adds the Origin Request Header for cross-origin requests:

GET /widgets/ HTTP/1.1
Host: api.mydomain.com
Origin: https://www.mydomain.com
[Rest of request...]
  1. The server checks the Origin request header. If the Origin value is allowed, it sets the Access-Control-Allow-Origin to the value in the request header Origin.
HTTP/1.1 200 OK  
Access-Control-Allow-Origin: https://www.mydomain.com  
Content-Type: application/json
[Rest of response...]  
  1. When the browser receives the response, the browser checks the Access-Control-Allow-Origin header to see if it matches the origin of the tab. If not, the response is blocked. The check passes such as in this example if either the Access-Control-Allow-Origin matches the single origin exactly or contains the wildcard * operator.

A server that responds Access-Control-Allow-Origin: * allows all origins which can be a large security risk.

Only use * if your application absolutely requires it such as creating an open/public API.

Intention

As you can see, the server has control over whether to allow the request or not depending on the origin of the request. The browser guarantees that the Origin request header is set reliably and accurately.

II. Preflighted requests

A preflighted request is the other type of CORS request. A preflighted request is a CORS request where the browser is required to send a preflight request (i.e. a preliminary probe) before sending the request being preflighted to ask the server permission if the original CORS request can proceed. This preflight request itself is an OPTIONS request to the same URL.

Since the original CORS request has a preflight request before it, we call the original CORS request preflighted.

Any CORS request has to be preflighted if:

  • It uses methods other than GET, HEAD or POST. Also, if POST is used to send request data with a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain, e.g. if the POST request sends an XML payload to the server using application/xml or text/xml, then the request is preflighted.
  • It sets custom headers in the request (e.g. the request uses a header such as X-PINGOTHER)

Source: Mozilla

Typical cases requiring preflighted requests:
  1. A website makes an AJAX call to POST JSON data to a REST API meaning the Content-Type header is application/json

  2. A website makes an AJAX call to an API which uses a token to authenticate the API in a request header such Authorization

This means it can be common for a REST API powering a single-page app to have the majority of AJAX requests preflighted.

Example flow
  1. A browser tab open to https://www.mydomain.com initiates an authenticated AJAX request POST https://api.mydomain.com/widgets with a JSON payload. The browser sends the OPTIONS request first (aka the preflight request) with the proposed Requested Method and Requested Headers of the main request:
OPTIONS /widgets/ HTTP/1.1
Host: api.mydomain.com
Origin: https://www.mydomain.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
[Rest of request...]
  1. The server respond back specifying the allowed HTTP methods and headers. If the original CORS request intended to send a header or HTTP method not in the list, the browser will fail without attempting the CORS request.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.mydomain.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Content-Type: application/json
[Rest of response...]  
  1. Since the headers and method pass the check, the browser sends the original CORS request. Note the Origin header is on this request also.
POST /widgets/ HTTP/1.1
Host: api.mydomain.com
Authorization: 1234567
Content-Type: application/json
Origin: https://www.mydomain.com
[Rest of request...]
  1. The response has the correct origin in Access-Control-Allow-Origin header so checks pass and control is handed back to the browser tab.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.mydomain.com
Content-Type: application/json
[Rest of response...]  


Advanced Configuration


You already saw a few headers in the previous examples that are used for CORS such as Access-Control-Allow-Origin and Access-Control-Allow-Methods, but there are more headers for finer control. Below is the full list of headers that control CORS.

Request Headers

Header Name Example Value Description Used in preflight requests Used in CORS requests
Origin https://www.mydomain.com Combination of protocol, domain, and port of the browser tab opened YES YES
Access-Control-Request-Method POST For the preflight request, specifies what method the original CORS request will use YES no
Access-Control-Request-Headers Authorization, X-PING For the preflight request, a comma separated list specifying what headers the original CORS request will send YES no

Response Headers

Header Name Example Value Description Used in preflight requests Used in CORS requests
Access-Control-Allow-Origin https://www.mydomain.com The allowed origins for this request as specified by the server. If it doesn’t match the Origin header and is not a *, browser will reject the request. If domain is specified, protocol component is required and can only be a single domain YES YES
Access-Control-Allow-Credentials true CORS requests normally don’t include cookies to prevent CSRF attacks. When set to true, the request can be made with/will include credentials such as Cookies. The header should be omitted to imply false which means the CORS request will not be returned to the open tab. Cannot be used with wildcard YES YES
Access-Control-Expose-Headers Date, X-Device-Id A whitelist of additional response headers to be exposed to the browser tab beyond the default headers no YES
Access-Control-Max-Age 600 Value in seconds to cache preflight request results (i.e the data in Access-Control-Allow-Headers and Access-Control-Allow-Methods headers). Firefox maximum is 24 hrs and Chromium maximum is 10 minutes. Higher will have no effect. YES no
Access-Control-Allow-Methods GET, POST, PUT, DELETE Can be * to allow all methods. A comma separated whitelist of allowed methods that can be used for the CORS request. YES no
Access-Control-Allow-Headers Authorization, X-PING Can be * to allow any header. A comma-separated whitelist of allowed headers that can be used for the CORS request. YES no

Wildcard (*) on Access-Control-Allowed-Headers, Access-Control-Allow-Methods, and Access-Control-Expose-Headers is not fully implemented in current browsers.
It is better to list out the headers or methods. See Chromium Bug


Common Pitfalls


1. Using * operator for Access-Control-Allow-Origin.

CORS is a relaxation of same-origin policy while attempting to remain secure. Using * disables most security rules of CORS. There are use cases where wildcard is OK such as an open API that integrates into many 3rd party websites.

You may improve security by having the API on a different domain.

For example, your open API https://api.mydomain.com could respond with Access-Control-Allow-Origin: * , but your main website https://www.mydomain.com still responds with Access-Control-Allow-Origin: https://www.mydomain.com

2. Returning multiple domains for Access-Control-Allow-Origin.

Unfortunately, the spec does not allow Access-Control-Allow-Origin: https://mydomain.com, https://www.mydomain.com. The server can only respond with one domain or *, but you can leverage the Origin request header.

3. Using wildcard selection like *.mydomain.com.

This is not part of the CORS spec, wildcard can only be used to imply all domains are allowed.

4. Not including protocol or non-standard ports.

Access-Control-Allow-Origin: mydomain.com is not valid since the protocol is not included.

In a similar way, you will have trouble with Access-Control-Allow-Origin: http://localhost unless the server is actually running on a standard HTTP port like :80.

5. Not including Origin in the Vary response header

Most CORS frameworks do this automatically, you must specify to clients that server responses will differ based on the request origin.

6. Not specifying the Access-Control-Expose-Headers

If a required header is not included, the CORS request will still pass, but response headers not whitelisted will be hidden from the browser tab. The default response headers always exposed for CORS requests are:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma
7. Using wildcard when Access-Control-Allow-Credentials is set to true

This is a tricky case that catches many people. If response has Access-Control-Allow-Credentials: true, then the wildcard operator cannot be used on any of the response headers like Access-Control-Allow-Origin.

Performance


An article on mitigating the performance penalties of CORS will be posted shortly.

Do you spend a lot of time debugging RESTful API bugs, like cross-origin issues?
Moesif makes debugging these issues much easier.


Learn More

Leave a Comment