CORS (Cross-Origin Resource Sharing): A Complete Guide
Tags: webdev, security, programming, cybersecurity
Published: 2021-03-09 08:28:24

Never be frustrated with CORS again. Learn what cross-origin resource sharing is, why it exists, and how to embrace it.

You can read the original post here: [CORS on AppSec Monkey](https://www.appsecmonkey.com/blog/cors/)

## What is CORS?
CORS, or *Cross-Origin Resource Sharing* is an opt-in browser feature that websites can use to relax the same-origin policy in a controlled way.

Browsers facilitate CORS via the `Access-Control-Allow-*` headers, which we'll get to soon.

I don't want you to be frustrated with CORS, so let's cover just a little bit of theory first. Specifically, let's take a look at the same-origin policy.

## What is the Same Origin Policy?
I've written about this at length in [here](https://www.appsecmonkey.com/blog/same-origin-policy/), but to give you the TL;DR, the same-origin policy is a set of design principles that govern how web browser features are implemented.

Its purpose is to isolate browser windows (and tabs) from each other.

For example, when you go to example.com, the website will not be able to read your emails from gmail.com (which you may have open in another tab). This is due to the workings of the same-origin policy.

## What is an Origin?
The definition of an origin is simple. Two websites are of the same origin if their scheme (http://, https://, etc.), host (e.g., www.appsecmonkey.com), and port (e.g., 443) are the same. You can find the definition in [RFC6545 - The Web Origin Concept](https://tools.ietf.org/html/rfc6454).

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/23r5bd88gdra0bk6kmv4.jpg)

### Implicit ports
If the port is not explicitly specified, it's implicitly 80 for `http` and 443 for `https`.

### Examples

Browsers consider these URLs to be of the *same* origin: 

- `https://www.appsecmonkey.com/`
- `https://www.appsecmonkey.com/blog/same-origin-policy/`
- `https://www.appsecmonkey.com:443/blog/same-origin-policy/`

And these are all of *different* origins: 

- `http://www.appsecmonkey.com/`
- `https://appsecmonkey.org/`
- `https://www.appsecmonkey.com:8080/`


## What is allowed by the same-origin policy, and what is not?

In general, writing and embedding are allowed, and reading is denied. How exactly this applies depends on the browser feature, but here are a few examples that concern CORS in particular. We can divide the examples into two categories: 

1. Sending the HTTP request is allowed, but accessing the response is not. This is the case for *simple* requests with whitelisted HTTP verb, headers, and content-type.
2. Even *sending* the request is not allowed. This is the case with *preflighted* requests with non-whitelisted HTTP verb, headers, or content-type.

### ✅ Allowed: Sending credentialed cross-origin GET, HEAD, and POST requests with XHR

The following will work. You will get an error, but the request will be *sent*. You can verify with your browser's developer tools, or better yet, set up a proxy tool such as [OWASP ZAP](https://www.zaproxy.org/) between your browser and the webserver to see what's going on.

```js
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET', 'http://b.local/');
xhr.send()
```

Developer tools reveal that the browser indeed sent the following HTTP request to the server.

```yaml
GET / HTTP/1.1
Host: b.local
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://a.local
Connection: keep-alive
Referer: http://a.local/
Cookie: SESSIONID=s3cr3t
Pragma: no-cache
Cache-Control: no-cache
```

### ❌ Not allowed: Inspect the XHR response

However, you will not be able to read the response that you get. This is the same-origin policy in action. Writing (sending the XHR request) is allowed, but reading the response is not.

```js
❌ Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://b.local/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).
```

### ✅ Allowed: Sending credentialed cross-origin GET, HEAD, and POST requests with fetch

Using fetch will work just the same. We can use JavaScript to submit an URL-encoded form to the webserver.

```js
fetch('http://b.local/', {method: 'POST', credentials: 'include', body: "foo=bar", headers: {
    'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},});
```

And the following HTTP request is sent: 

```yaml
POST / HTTP/1.0
Host: b.local
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://a.local/
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Origin: http://a.local
Content-Length: 7
Connection: keep-alive
Cookie: SESSIONID=s3cr3t
Pragma: no-cache
Cache-Control: no-cache
```

### ❌ Not allowed: Inspect the fetch response

It's the same with fetch. Sending the credentialed, cross-origin POST request was permitted, but accessing the response was denied.

```js
❌ Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://b.local/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).
```

We'll get to what the `Access-Control-Allow-Origin` thing is in a minute, but let's look at a couple more scenarios first.

### ❌ Not allowed: Sending PUT, PATCH, DELETE, etc. requests

Only specific HTTP verbs are allowed by default (GET, POST, HEAD, and OPTIONS).

```js
fetch('http://b.local/', {method: 'PUT', credentials: 'include'});
❌ Access to fetch at 'http://b.local/' from origin 'http://a.local' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```

Note that this time the browser didn't send the request at all. Also, note that the error is now different. It's talking about a *preflight* request. We'll get to that soon.

### ❌ Not allowed: Sending arbitrary headers
Trying to send a request with arbitrary headers is not allowed by the same-origin policy.

```js
fetch('http://b.local/', {
    method: 'POST', credentials: 'include', headers: {
        'Foo': 'Bar'
    },
});
❌ Access to fetch at 'http://b.local/' from origin 'http://a.local' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```

Only the [CORS-safelisted headers](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header) are allowed by default. And they are: 

- Accept
- Accept-Language
- Content-Language
- Content-Type

### ❌ Not allowed: Sending JSON requests

While the `Content-Type` header is safelisted, it is so with restrictions. Specifically, only the following values are acceptable: 

- application/x-www-form-urlencoded
- multipart/form-data
- text/plain

```js
fetch('http://b.local/', {
    method: 'POST', credentials: 'include', headers: {
        'Content-Type': 'application/json'
    },
});
❌ Access to fetch at 'http://b.local/' from origin 'http://a.local' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```

Again with the preflight.

Alright, now that you understand the restrictions that same-origin policy places upon you, I'll tell you what CORS is and how it helps you get around those limitations in a controlled way.

## Cross-Origin Resource Sharing (CORS)
CORS can lift the above restrictions. It's not a browser security mechanism like the same-origin policy. CORS is a browser *insecurity*-mechanism, so read carefully and use it with consideration.

Browsers implement CORS as a set of four HTTP response headers, which we'll get to right now.

### Access-Control-Allow-Origin
The first header is `Access-Control-Allow-Origin`. Developers can use it to grant cross-origin requests the read-permission to a website's resources (which is denied by the same-origin policy by default).

Possible values are: 
- a specific origin such as `https://www.appsecmonkey.com`
- a wildcard, allowing any domain

For example: 

```yaml
Access-Control-Allow-Origin: https://wwww.appsecmonkey.com
```

Or: 

```yaml
Access-Control-Allow-Origin: *
```

It's either-or. Something like this would **not** work: 

```yaml
Access-Control-Allow-Origin: *.appsecmonkey.com
```

Also, specifying multiple origins is **not** allowed, so the following would **not** work either.

```yaml
Access-Control-Allow-Origin: https://foo.appsecmonkey.com, https://bar.appsecmonkey.com/
```

#### CORS wildcard restrictions
The wildcard will not work in combination with `Access-Control-Allow-Credentials: true`, which you'll learn about shortly. Just remember this limitation.

#### How to specify multiple CORS origins?
The spec doesn't support multiple origins. However, in practice, it has been solved by generating the `Access-Control-Allow-Origin` header dynamically in your code. This workaround is facilitated by the `Origin` request header, which browsers send in all POST and CORS requests.

Note that there is a security hazard here. It's best to use a well-established CORS library for your development framework of choice instead of implementing a homemade solution.

At any rate, you should have a *strict whitelist* of possible origins. **Do not** implement logic that, e.g., checks if the request origin *contains* a specific string or *starts with* one.

### Access-Control-Allow-Credentials

By default, CORS doesn't allow credentialed requests (that include the browser user's cookies). After all, credentialed CORS requests effectively give the websites to whom the privilege is granted full read and write control of the browser user's data in the application.

If you still want to enable it, you can use the `Access-Control-Allow-Credentials` header like so: 

```yaml
Access-Control-Allow-Credentials: true
```

Just note the limitation mentioned above: this does not work in conjunction with `Access-Control-Allow-Origin: *`.

#### Important caveat about SameSite cookies
Browsers are starting to adopt SameSite cookies in Lax mode as the default. I've written about SameSite cookies at length [here](https://www.appsecmonkey.com/blog/samesite-cookies/).

What this means is that cookies are by default protected from cross-site interactions. If you are banging your head against the wall trying to figure out why the browser doesn't send your cookie even when you have all the proper headers in place, it might be SameSite cookies in action.

### Access-Control-Allow-Headers
If you want to send custom headers or lift the restriction on the `Content-Type` header to, e.g., send JSON requests, you can use the `Access-Control-Allow-Headers` to do so.

```yaml
Access-Control-Allow-Headers: content-type
```

### Access-Control-Allow-Methods
Finally, suppose you want to enable other HTTP verbs than GET, POST, HEAD, and OPTIONS. In that case, you have to use the `Access-Control-Allow-Methods` header. 

```yaml
Access-Control-Allow-Methods: GET, POST, HEAD, PUT, PATCH, DELETE
```

In fact, even if you only want to allow, e.g., POST requests, you are still required to return `Access-Control-Allow-Methods` if there are any other factors that cause your request to be *preflighted*, which we'll talk about next.

### Preflight

Simple requests with the whitelisted HTTP verbs, headers, and content-type are always *sent*. Still, the website is forbidden access to the response data if the response doesn't contain the appropriate `Access-Control-Allow-Origin` header.

But how does the browser know whether it is allowed to *send* a PUT request or not? If the answer to the question "can I send a PUT request" is in response to the PUT request, doesn't this create a chicken and egg problem? That's a great question, and the answer is simple: we send two requests.

The browser first sends an `OPTIONS` request with the `Origin` header, and then looks at the response headers for that request. If `PUT` is allowed (in `Access-Control-Allow-Methods`), only then the preflight succeeds, and the browser sends the desired PUT request.

Of course there are other reasons why the preflight might fail. For example, you might be trying to send a request with credentials included, but the webserver doesn't return `Access-Control-Allow-Credentials: true` in the preflight HTTP response.

This first OPTIONS request is aptly named the [preflight request](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request).

### CORS in action

Let's revisit one of the tests we made earlier, but this time, http://b.local/ returns the following HTTP response headers: 

```yaml
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Methods: GET, HEAD, POST, PUT
Access-Control-Allow-Origin: http://a.local
```

### ✅ Allowed: Sending credentialed cross-origin GET, HEAD, POST, and PUT requests with fetch and reading the response

Now we have complete control of the cross-origin page.

```js
fetch('http://b.local', {method: 'PUT', credentials: 'include', headers: {
        'Content-Type': 'application/json'
    }}).then(function (response) {
	return response.text();
}).then(function (html) {
	// This is the HTML from our response as a text string
	console.log(html);
});

<!doctype html>
<html lang=en>

<head>
    <meta charset=utf-8>
    <title>b.local</title>
    ...
```

**☠ Security Impact**: If you specify CORS headers like this, you are giving the allowed origins complete control over your website, including any authenticated user data and functionality. The same-origin policy is there to protect you, so think carefully before opting out of it.

## Using CORS
If your requirements are simple, you can just add the static headers to your application/web server configuration.

However, if you have to deal with multiple origins, it's best to use a CORS library for your development framework. For example, this is how you configure CORS in flask: 

```python
CORS(
    app,
    origins=['http://a.local', 'http://c.local'],
    allow_headers=['content-type'],
    supports_credentials=False,
    methods=['PUT', 'PATCH', 'DELETE']
)
```

## Exposing headers
By default, only the [CORS-safelisted response headers](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header) are exposed to to JavaScript code in CORS requests. So if your webserver returns the header `Foo: Bar`, even CORS requests won't be able to access it.

If you *want* browsers to access this header, you can do so via the `Access-Control-Expose-Headers` like so: 

```yaml
Access-Control-Expose-Headers: Foo
```

## Conclusion
Browsers consider two websites to be of the same origin if they have the same scheme, host, and port.

The same-origin policy places several restrictions over cross-origin interactions. Most notably for CORS: 

1. Sending "simple" requests with a whitelisted HTTP verb, headers, and content-type is allowed, but accessing the response is not.
2. Sending "preflighted" requests with non-whitelisted HTTP verb, headers and content-type are not allowed at all.

CORS, or Cross-Origin Resource Sharing, is a browser *insecurity*-mechanism for your web application to opt-out of some of these restrictions in a controlled way.

However, CORS should be used with consideration and implemented using a well-established CORS library for your development framework of choice.

## Get the web security checklist spreadsheet!
[![Subscribe](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3iqd1ef4a5a0icyvvk65.jpg)](https://eepurl.com/hqAGt5)
 ☝️ [Subscribe](https://eepurl.com/hqAGt5) to AppSec Monkey's email list, get our best content delivered straight to your inbox, and <b>get our 2021 Web Application Security Checklist Spreadsheet for FREE</b> as a welcome gift!

## Don't stop here
If you like this article, check out the other application security guides we have on [AppSec Monkey](https://www.appsecmonkey.com/) as well.

Thanks for reading.