What is CORS?

tim-armstrong-headshot

Tim Armstrong|June 15, 2021

At this point, I think I've probably seen most ways to (mis)configure CORS headers. So let's talk about what they are, how they protect you and your customers, and how to get them set up correctly.

As you've possibly already come across by now, CORS is an acronym for Cross-Origin Resource Sharing, but what does that actually mean? What is CORS? Well, if we go by the Wikipedia definition, "[CORS] is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served," then you'd be forgiven if you were more confused than before you'd read that sentence.

Before we get into defining CORS, it's best to know what came before, as it still defines the default behavior and is probably why you're reading this now. This precursor to CORS was called the "Same-Origin" policy. In short, it dictates that when your browser loads a script (like a button handler, or some async widget) from a particular (sub)domain that the script can only make requests to the (sub)domain that it originated from.

Cross-Origin Resource Sharing

So then, what is CORS? Simply put, CORS is the mechanism that provides the ability to alter the behavior of this policy, enabling you to do things like hosting static content at www.example.com and the backend API at api.example.com. This kind of request would be called a Cross-Origin request, as a resource from one subdomain is requesting a resource from another subdomain.

This is all controlled through preflight requests that exchange a set of HTTP request headers and corresponding response headers collectively referred to as "CORS Headers", each of these headers modifies a different element of the Same-Origin policy to loosen the limitations it imposes.

There's a lot of terrible advice out there (especially on popular forums) on how to set this up where the answers generally include some variant of brutally setting wildcard "*" response headers regardless of the request headers provided in the pre-flight request. This article attempts to dispel some of the common misconceptions about Cross-Origin Resource Sharing and provide useful advice on how to get things working correctly.

How Does CORS Work?

As mentioned above the CORS workflow starts when a script loaded from one origin attempts to make a request to another origin (thus the name Cross-Origin Resource Sharing).

What is CORS? image

This workflow begins with the browser automatically making a preflight request to the external web server. This preflight request uses the HTTP method OPTIONS and has several HTTP headers that we'll go into detail on later. The external web server should then validate these preflight request headers to ensure that scripts from that origin are allowed to make the actual request to the resource using the nominated request method and custom request headers specified in the preflight request headers.

Once verified the external web server should then respond with its own set of HTTP headers. These response headers define the range of acceptable origins, request methods, custom headers, whether or not it's acceptable to send any credentials (such as cookies, authentication headers, etc.), and how long the browser should keep the response for. This allows the browser to keep that response cached as a form of pre-validation for any future requests that the script might wish to make.

So What Are the Request Headers, and What Do They Do?

Access control request headers are fairly straight forward and for the most part pretty self-explanatory.

  • `Origin` – The (sub)domain that the script making the request was served to the browser from.

  • `Access-Control-Request-Method` – The method that the script would like to use in the actual request to follow.

  • `Access-Control-Request-Headers` – The custom headers that the browser expects to send along with the actual request to follow.

At a Bare Minimum

The `Origin` should be checked against an access list by the server to confirm that scripts from that origin are acceptable. This isn't going to stop 100% of attacks, but it should at least slow down and discourage attackers and significantly reduce the risk of an automated malicious advert attack being successful. If the `Origin` header isn't a match to your access list, then it's a good canary that an attack might be incoming.

The other easy thing to validate is that the `Access-Control-Request-Method` is actually a supported HTTP method used in your API. As with the `Origin` header, if it's not something your API supports, then you know something is going on.

Helpful Canaries

Setting up validation failure alerts for both of these headers gives you a reliable early warning that an attack is incoming, allowing you to act quickly to protect your users.

The `Access-Control-Request-Headers` request header is unfortunately not very reliable, as the actual request might contain some, none, or more custom headers than the browser has recognized. So it's fairly safe to ignore it if everything else is valid, but you probably want to log it if either of the others fails, as it might be a useful signature for future requests originating from the attacker.

Access Control Allow Headers and How to Respond to a CORS Request

The access control allow headers are a little more complicated than the request headers, this is mostly because of a lack of proper implementation of the standard in most browsers. But before we drill down into the problems and how to avoid them, let's first talk about what the headers are and what they do.

  • `Access-Control-Allow-Origin` – Provided that the `Origin` request header matches your access list then this header should reflect that request header's content.

  • Access-Control-Allow-Credentials – This header is a boolean indicating to the browser whether or not it is acceptable for code from this Origin to send authentication credentials such as cookies or Authorization headers.

  • Access-Control-Expose-Headers – This is a comma-separated list that indicates to the browser which headers from the server's response to the actual request should be exposed to the script making the request.

  • Access-Control-Max-Age – The max-age header indicates how long the browser should retain the response to this cross-origin request's preflight check in its cache to reduce the overhead of future cross-origin requests.

  • Access-Control-Allow-Methods – This header lists all of the methods that scripts coming from the (sub)domain stated in the Origin header should be allowed to make.

  • Access-Control-Allow-Headers – If the preflight request contains an `Access-Control-Request-Header` then this header should either reflect that content to the browser or respond with a wildcard.

About Those Caveats...

`Access-control-allow-origin`

At first blush, it might seem like the `Access-Control-Allow-Origin` header would support something like a wildcard subdomain, or a comma-separated list that you could statically set to match all of the domains and subdomains that you want to support, as this would drastically reduce the number of times the browser might need to do preflight requests in a session. However, this is unfortunately not the case, and doing so will result in undefined behavior. This tends to be the origin of most of the bad advice on forums like stack-overflow. Because as strange as it might seem the standard does support a generic wildcard that simply disables this check in the browser altogether, so people jump on this as the easy answer. Putting user data at risk as a result.

What's So Bad About The Wildcard?

As mentioned earlier, setting `Access-control-allow-origin` to `*` effectively disables the same-origin policy. This means that the browser will allow almost any request to that cross-origin resource from any script that happens to be loaded. This might not seem so bad, because you trust all of the code you put on your site, right? But that's not the whole story, because the browser is now not filtering the origins, this means any code on any site (including malicious phishing sites) can actually make a request to that resource.

Now, given that modern browsers are at least a little bit security conscious, if you did attempt to follow the wildcard copy-pasta that is all over popular forums and needed to use credentials such as Authorization HTTP headers or cookies, your cross-origin request will fail. This is because, in an attempt to at least partially fix this class of vulnerability, browsers don't allow you to set the `access-control-allow-credentials` header if the `access-control-allow-origins` is set to a wildcard. Which ultimately lead to the next bad idea that someone had: Sending your authentication token as a custom header – completely exposing user data to any malicious code.

Can't I Just Reflect the Origin?

This solution while seeming smart on the surface, without any validation of the origin field, this blind reflection is actually considerably worse than the wildcard as it completely bypasses the browser's prevention of setting both the `access-control-allow-credentials` header and alongside a wildcard `access-control-allow-origins` (because it's now not a wildcard from the browser's perspective). Adding authentication credentials to the list of potentially exposed data.

Principals of an Attack

The risks and context of an attack depend on the nature of the misconfiguration and how you're authenticating requests. Modern browsers do their best to mitigate the impact of the most egregious configuration errors (i.e. using the wildcard policy). However, there are equivalents that still get through.

Let's consider the worst-case scenario defined above, blind reflection as it is the easiest to exploit.

In order to exploit a blind reflection, all you need is for a victim (one of the target's clients) to browse any site that you can control or inject malicious code into, from there your code can make requests as the victim with the browser transmitting any authentication cookies it needs to perform those requests.

What is CORS? image

Diagram of a successful attack profile.

So what happened in this example? First, the victim navigates to a malicious site (this could be due to a phishing e-mail, or even just browsing a website with a malicious advert). When the page loads the browser runs the javascript on the page. This "evil.js" then makes a request to the vulnerable target to which the browser (having validated the CORS headers from the target) dutifully attaches the victim's cookies for that resource. When the target then responds, "evil.js" then forwards this response to the malicious website. 

The attack doesn't have to stop here either, include some C&C code to evil.js and it could start relaying any command on behalf of the malicious site to the vulnerable site.

As others have shown as far back as 2016, this was a fairly common vulnerability, and only takes one mistake in some regex. Unfortunately, this is seemingly still the case.

What is the Best Practice to Enable CORS Then?

In order to ensure both that the script comes from an origin that you expect to be making valid requests to the cross-origin resource and that the browser won't just allow every script it loads to contact that resource. You should be validating all of the access control request headers against appropriate access lists. The implementation of this can be a little tricky, especially with the origin header, and the web is full of minor misconfigurations in parsing methods and bad regex that leaves all sorts of sites exposed to manipulation by an attacker. But, if you keep it simple, you can do it safely with a secure string comparison against an array of trusted values. If it doesn't perfectly match your lists then respond with a `403 Forbidden`.

If we want to support more than one `Origin,` then until browsers support a list of origins in the `access-control-allow-origin header,` we don't really have a lot of choice but to reflect the validated `Origin` request header into the `access-control-allow-origin header.`

Libraries, Frameworks, & Reverse-Proxies

For most languages and web frameworks there is a pre-built solution that should handle all of this for you securely so that you don't have to. But there are a few that don't implement proper validation, so you do need to validate the library operates as expected before trusting it.

Another alternative is to use an established web server like Nginx or Apache as a reverse proxy and use its filter rules to suitably reflect the headers if they satisfy the validation. Though, since these use regex for their validation filters, you have to be very careful when constructing your search string to ensure that you are safely validating the entire string matches your expectations. As a lot of companies that go this way seem to manage to introduce mistakes here.

Further Reading

For more background on exploiting misconfigured Cross-Origin resources and Tutorials on how to get it set up for correctly the framework you use, keep an eye open for more articles here. 

In the meantime, you might want to read one of our other articles on XSS in:

Mozilla has an excellent set of explainers that break down the jargon from the standards into plain English. We'd recommend taking a look at their explainers on:

If you're an experienced pen-tester then you might also want to read root4loot's article on Abusing improper CORS origin validation.

This post was written by Tim Armstrong. Tim has worn many hats over the years, from "Dark Lord of Network Operations" at Nerdalize to "Lead Software Engineer" at De Beers. These days, he's going by "Consultant Engineer & Technical Writer." You can find him on Twitter as @omatachyru, and at plaintextnerds.com.


Tim Armstrong  |  June 15, 2021