IT TECHOLOGY

⌘K
  1. Home
  2. Docs
  3. IT TECHOLOGY
  4. SECURITY
  5. DEFINITIONS
  6. CORS 2

CORS 2

Cross-Origin Resource Sharing (CORS) is a standard for controlling access to web content across different origins. For an introduction to CORS, see e.g. What is CORS? Complete Tutorial on Cross-Origin Resource Sharing.

CORS is notorious for having some nuances that are challenging to pick up on. The standard also contains certain quirks which can make implementing CORS less straightforward. Additionally, most developers only encounter CORS as a hurdle that needs to be configured “until it works”.

This document is an attempt to clarify some common blind spots applicable to us, but does not cover all of the CORS spec. Raising additional questions and concerns is welcome, so the text can be expanded and improved upon.

Same-origin policy

By default, the browser implements the same-origin policy (SOP), allowing a website to communicate freely with itself, but limiting communication with other sites.

The SOP allows cross-origin communication for certain “simple” requests. For example, embedding an image on the page, or submitting a regular, old-school form count as “simple”. If you need more than that (e.g. read & write to a modern JSON API, send custom headers, etc.), the browser needs you (the API author) to explicitly allow it using CORS.

It can help to think of CORS as not a “security feature” per se. Rather, SOP is a security feature, and CORS is a way of defining exceptions to the SOP and the default security guarantees it provides. This leads to a couple of observations:

  • Lack of CORS headers is not a security problem (other than the fact that it may prevent the app from working at all). Overly permissive CORS headers are a security problem, because they may strip away more of the SOP than intended.
  • CORS only matters in the browser context (or browser-like user agents that implement SOP). It does not affect server-to-server requests – you can curl away at your hearts content to a URL regardless of its CORS configuration.

Credentials

From a security standpoint, the biggest differentiator between CORS configurations is typically whether they “allow credentials” or not. This is done with the following header:

Access-Control-Allow-Credentials: true

The implicit, default value is false, so the Access-Control-Allow-Credentials (ACAC) header can be left out entirely if not needed.

What’s “credentials” in this context? Short answer: cookies. Longer answer: credentials managed by the user agent (browser), that would automatically be sent with each request – in practice, that’s cookies, HTTP Basic, Digest and NTLM authentication, as well as client-side TLS certificates.

If a cookie-authenticated API on https://api-origin.test needs to be accessible from https://frontend-origin.test, it likely needs CORS set up with the ACAC header set to true. These are the cases where a misconfiguration of the rest of CORS could have dire consequences. If a 3rd-party origin is allowed to make requests to the API with the user’s cookies included, they are the user, and can read and write any data the user can.

If the API uses an authentication method other than cookies, it probably does not need the ACAC header. The typical example is an API with JWT authentication, where the JWT is “manually” managed by the frontend, and added to each request as an Authentication: Bearer <...> header. Since the Bearer token is not automatically added by the browser, it’s not affected by ACAC.

This is worth reiterating, because it’s a common point of uncertainty:

  • If an access token must be manually added to a request header, it’s not the type of “credential” that ACAC refers to.

For our API example, this is a security benefit. A malicious 3rd-party origin can’t trick the browser into sending authenticated requests, because the browser doesn’t manage the authentication. The 3rd-party origin would need to somehow have direct access to the actual JWT, and if that were the case, all bets are off and browser controls like SOP & CORS no longer matter. Once the attacker has access to the JWT, they can just make authenticated requests server-to-server, or through their own browser.

Origin

The browser send an initial preflight OPTIONS request to determine whether the planned cross-origin request is allowed. As with other requests, the Origin header is included in this request. The browser then expects an exact match in the Access-Control-Allow-Origin (ACAO) header of the response:

Access-Control-Allow-Origin: https://frontend-origin.test

One shortcoming of CORS is that the ACAO header can only include one origin at a time. If you have e.g. three different frontend domains, the server must dynamically send the right ACAO header to match the request, but deny / ignore other origins.

Often, you may not even know the exact list of origins ahead of time – it could be you want to allow all origins of the form https://frontend.*.company.test. Since partial wildcard matching does not exist in the CORS spec, the matching must be done server-side and the exact origin returned. (The single-asterisk wildcard origin is different, and discussed below.) One thing to look out for is to not make the matching overly permissive – it should not accidentally match e.g. https://frontend.foo.company.test.attacker.test.

Wildcard origin

At the time of writing, we use the wildcard origin for our API:

Access-Control-Allow-Origin: *

That sound risky, aren’t we exposing the API to cross-site requests from malicious origins?

This is a valid question. The answer is that yes, now any site’s frontend can attempt “complex” requests to the API (in addition to the “simple” requests allowed by SOP), and read the responses. However, since the API is Bearer-authenticated, all requests without the right JWT will be met with HTTP 401 or 403 – exactly as if the request was sent server-side.

If the API was cookie-authenticated, allowing any origin would of course be much more dangerous. In that event however, we would not be able to use the * wildcard, because of how it’s defined in the spec: when using the wildcard ACAO, the ACAC header has no effect. To allow cookies to be sent, you must instead explicitly reflect the allowed origin in ACAO.

Due to the intricacies and clunkiness of CORS, these distinctions are easy to miss, even for experienced web devs and security professionals. It should not come as a surprise if the wildcard is raised as an issue in a security audit. However, without a proof-of-concept with demonstrated impact, it may just be boilerplate. Real impact usually arises when there is some other implicit authentication happening outside the browser, such as an intranet site authenticating requests by IP address (which would be risky in and of it itself).

What about performance, then? Are we opening ourselves up to DoS / DDoS attacks by allowing anyone to make requests, even if they are unsuccessful? The answer is no, not really. A botnet of compromised webcams spamming HTTP requests do not care about SOP and CORS. In the unlikely event we are hit with an attack from, say, malicious ads spamming unauthenticated requests from real user browsers (and that the attack only works with “complex” but not “simple” requests), the right control is to avoid processing such requests (e.g. using Cloudflare), not to hope for unrelated controls like SOP & CORS to prevent them from being sent.

Last, even if we preferred to use explicit, reflected origins, it may not be possible in all cases. Validating the origin is difficult with certain frontends, such as hybrid web-and-native apps. This could be e.g. if they are loaded over the file:// protocol, or in some iframe setups, setting the Origin header to null. Responding with an ACAO value of null would open up to requests from anywhere, the same way as the wildcard, except with the disadvantage that ACAC is disallowed for * but allowed for null.

Errors

If the server responds without the expected CORS headers, the request (preflight and/or actual) will fail, and the browser will log an error to the console.

One thing to note is that this could also happen for other reasons than a fault in the CORS configuration itself. For example, if a backend service experiences downtime, the HTTP server or CDN in front of it may respond with a default HTTP error response (e.g. 500, 502, 503, 504), likely without any CORS headers.

The initial error might lead the developer down a CORS debugging rabbit hole, when the actual error may have been anything from a syntax error, to a database running out of disk space. This is an additional source of developer uncertainty around CORS, since it occasionally creeps up seemingly from nowhere. Being aware of this error message mismatch can help in noticing the actual cause more quickly.

How can we help?