5 CORS Headers Your API Needs for Web Security

2022-08-02 - 8 min read

Cross-origin resource sharing (CORS) is a standard that defines a set of HTTP headers to enhance web security. It is part of the fetch standard. These headers are a necessary part of APIs that serve JavaScript frontends.

Why do we need CORS?

CORS is a security mechanism in the first place, so it makes sense first to ask what risk it tends to mitigate. We'll start with an example to explain this. Let example.com be a headless website that uses an API endpoint at example.com/api/account. That endpoint returns details of the logged-in user based on a session cookie. This is a practical and valid use case, but there is one problem.

Let evil.com be a site that requests exmple.com/api/account using a request initiated by JavaScript. Will it return the user details when the visitor is logged in at example.com? And hence able to steal the visitor's private information returned by that endpoint? Without CORS, the answer is yes. With cors, that answer is… dependent on how you use it.

There is not always a need to set CORS headers. There is no need for the frontend of example.com in this specific example, as long as it's running on the same domain as the API. However, if you introduce a subdomain, for instance, docs.example.com, that needs the API, then you need to set CORS headers. In that case, it is a cross-origin request, which is blocked by default. You can allow these requests with CORS headers.

Even if you do not need cross-origin requests, you should check your headers to ensure your API is not vulnerable to sensitive data exposure.

Which CORS headers do you need for an API?

Access-Control-Allow-Origin

The most important one for your API is the "Access-Control-Allow-Origin” header. This header tells the browser which origins (protocol, domain, and port) are allowed to access your API. If you don't set this header, then only browsers on the exact origin as your API will be able to access it.

Many developers are tempted (or advised) to set this header with the value "*”. This will allow access to all external sites. This can be a valid use case, but it is not what you want most of the time. If docs.example.com needs to access an endpoint at example.com, then that API should use "docs.example.com” as the value for this header.

Access-Control-Allow-Methods

Another important CORS header is the "Access-Control-Allow-Methods” header. This header tells the browser which methods are allowed when accessing your API. The methods allowed by default are GET, POST, and HEAD. However, if you want to allow other methods, such as PUT or DELETE, you need to set this header.

Access-Control-Allow-Header

This header tells the browser which headers are allowed when accessing your API. There are a few safelisted headers that are always allowed. These are Accept, Accept-Language, Content-Language, and Content-Type. if you want to enable other headers, you will need to set this header. You may also use the wildcard here, which accepts all headers except Authorization, which you must add explicitly.

Access-Control-Allow-Credentials

This header tells the browser whether or not it should send cookies, basic auth, or client certificates along with cross-origin requests. The default is not to send any credentials, but if you set this header to "true” then credentials will be sent. In our example, we need this header for the session cookie. The use of authentication across origins is prone to many pitfalls, as we will discuss later.

Access-Control-Max-Age

This header tells the browser how long (in seconds) it should cache the CORS headers. This is only applicable to preflight requests — see later. This can be valuable if you know that your CORS configuration will not change often. The default is 5 seconds, but you can increase API performance by extending the cache lifetime.

About the CORS (preflight) requests

When a website executes a fetch call to make a GET request to an external API, it will send the request as usual. When the browser receives the response, it will inspect the headers first before making the response available to JavaScript. So, denying the request with CORS does not actually deny the request, but only receiving the response in this case. This is designed on the assumption that GET requests will not change anything on the endpoint, but for other requests, like a POST with a JSON body, we certainly need a different mechanism.

A so-called preflight request precedes non-GET requests. This request will use the OPTIONS method and include a few headers to provide further information. The most crucial header here is Origin, which will contain the domain of the website initiating the request. You may use information from these headers to decide on allowing this request, but beware of caching issues — see next section.

What are common pitfalls?

Setting the CORS headers too permissive.

The first hits on Google for CORS issues will inevitably lead to the advice to set Access-Control-Allow-Origin to *. Hopefully, they will tell about the risk too. You should select the correct headers from the start to prevent being bitten later by these security flaws.

Include protocols and port numbers in origin

To access your API, the Origin names must include the protocol and non-standard port number. Valid origins are:

http://localhost:3000

https://docs.example.com

Credentials do not work with wildcard domains

Using "*” for Access-Control-Allow-Origin, and true for Access-Control-Allow-Credentials does not have any effect. The standard is more strict on this point. To allow credentials, you must enable the origin explicitly. Therefore, allowing credentials for all domains can only be done by reading the Origin request header and copying the value to Access-Control-Allow-Origin response header.

Credentials need to be included explicitly in the fetch call

If you want to send the credentials (cookies or basic auth), then you need to set the credentials option to include.

await fetch(‘https://example.com/api/account', { credentials: ‘include' });

Credentials need a secure environment

Sending credentials requires that both the initiator (sender) and API server (receiver) are in a secure environment. That is, they run SSL with a valid certificate (not a self-signed cert). Some browsers treat localhost as secure too, but not all.

Allowing explicit Authorization header

If your frontend application holds the API authentication token (for example an OAuth2 access token), you might include it in the headers option of your fetch call. In this case, you do not need the Access-Control-Allow-Credentials, but you need to add "Authorization” to Access-Control-Allow-Headers instead. Furthermore, you must include that name explicitly. A wildcard in this header will not match "Authorization”. This is again an exception in the specs to improve security.

Add Vary header when reading Origin to avoid caching issues

As suggested, you can use the Origin header on the server to produce different response headers per origin. However, proxy servers (including CDNs) might cache the requests, including these headers. Therefore, it is mandatory that you set the Vary header with the value "Origin” when doing so. This tells the proxy to include the Origin request header in the cache key.

CORS does not prevent the processing of all POST requests

HTTP POST requests that could be sent from an HTML form do not use preflight requests (given some conditions). It puts your API at risk because it might still be vulnerable to CSRF attacks — even after setting CORS headers. When the CORS standard was introduced, they did not want to break the existing HTML forms. However, this is a pity security-wise because it likely led to many vulnerable APIs.

So, suppose your API allows POST requests with authentication and accepting form data (which is not uncommon). In that case, you must validate the CORS headers on the server before executing the request, or prevent the request by other means, such as a CSRF token.

Setting CORS headers in Flowlet

We simplified the CORS settings in Flowlet. The configuration of an HTTP endpoint flow contains a dropdown with three options:

  • Disallow cross-origin requests (default)
  • Allow all cross-origin requests
  • Allow from selected domains

Additional configuration options appear after changing this setting. These let you set the allowed domains, headers, and cache lifetime. The option to allow credentials only appears when you set this option to Allow from selected domains. It prevents one of the pitfalls; we cannot use this option with wildcards.

CORS settings in Flowlet

Flowlet automatically takes care of the Vary header and server-side validation of non-preflighted requests. Hence it will prevent the last two pitfalls.

Conclusion

This article looks at CORS headers and why they're important in building APIs and API security. The many exceptions give rise to common pitfalls. It is, therefore, not a quick-and-easy task to implement CORS directly. APIs built using Flowlet benefit from a simplified configuration and server-side checks for improved security.

Can't wait to write secure APIs with Flowlet?