CORS FAQ

What is CORS?

CORS is an acronym. It stands for ‘Cross-Origin Resource Sharing’.

If you are involved in creating, configuring or maintaining a website then you may need to know about CORS.

Web browsers have a security feature known as the same-origin policy. This policy prevents code on one website from talking to a different website unless certain conditions are met.

Roughly speaking you can think of ‘origin’ as meaning ‘website’ or ‘server’. So when we talk about ‘same-origin’ we mean ‘same-website’ or ‘same-server’. To find out more about exactly how origin is defined see What does ‘origin’ mean?.

In short, CORS is a way to turn off this security feature to allow AJAX requests to a different site.

The site making the AJAX request can’t just turn off security. That would be back-to-front. As far as the browser is concerned that site might be malicious and can’t be trusted. It is the server receiving the AJAX request that decides whether to allow CORS.

Why am I getting a CORS error when I’m not even using CORS?

If your website has attempted to make an HTTP request to a different site then the browser will try to use CORS. You don’t have a choice, it happens automatically. If the target server doesn’t have CORS enabled then the request will fail and the browser will log a CORS error to the console.

Using the same domain with two different port numbers is not sufficient to avoid CORS. The port number is considered to be part of the origin when the browser decides whether or not to use CORS.

What does ‘origin’ mean?

Consider the following URL:

http://www.example.com:3456/path?query=text

The origin for this URL would be http://www.example.com:3456. It includes the scheme, hostname and port.

http://www.example.com:3456
<scheme> :// <hostname> [ : <port> ]

The port may be omitted if it is the default port for the scheme, so 80 for http or 443 for https.

When the browser makes a CORS request it will include the origin for the current page in the Origin request header.

In JavaScript you can access the current page’s origin using location.origin.

How does CORS work?

Let’s start with the basics.

There are two questions that the browser needs to answer:

  1. Are you allowed to make the request at all?
  2. Are you allowed to access the response?

There are some requests that are considered simple enough and safe enough that the first stage is skipped. We’ll come back to that later.

For the second question, the browser assumes by default that you can’t access the response for a cross-origin request. That includes the response body, the response headers and the status code.

In addition to the usual request headers the browser will also include an Origin header, containing the origin of the page making the request. Note this is added automatically by the browser, you can’t add it yourself.

The server then needs to grant permission for the response to be exposed to the client-side code. It does this by including the response header Access-Control-Allow-Origin. The value of this header must be identical to the value of the Origin header it received. It can also use the special value * to allow any origin. Any other value will cause the CORS check to fail in the browser.

It is really important to understand that Access-Control-Allow-Origin is a response header sent by the server. It is NOT a request header. It is a very common mistake to try to set this header on the request, which won’t help at all.

// Don't copy this code, it is wrong!!!
axios.get(url, {
  headers: {
    // This is in the wrong place. It needs to be on the server.
    'Access-Control-Allow-Origin': '*'
  }
})

In addition, only a limited subset of the response headers will be exposed by default. See Why can’t I access the response headers in my JavaScript code? for more information.

If a request is not deemed ‘simple’ and ‘safe’ then it must first undergo a preflight check to determine whether to allow the request at all. See What is a preflight request? for more information.

Why am I seeing an OPTIONS request instead of the GET/POST/etc. request I wanted?

That is the preflight request.

See What is a preflight request? for more information.

What is a preflight request?

The term is a reference to the preflight checks carried out by pilots.

It is a request generated automatically by the web browser. It is used to check whether the server is willing to allow the original request.

Before CORS existed you couldn’t make AJAX requests to other servers. However, you could make requests by other means, such as by submitting a form or including a <script src="..."> in your page.

Those alternative means of making requests had some limitations. e.g.:

  • You could only make GET and POST requests.
  • There was no way to set custom request headers.
  • A POST request could only have a content-type of application/x-www-form-urlencoded, multipart/form-data or text/plain.

When CORS was introduced it was important to ensure that no new security holes were opened up. If an AJAX request tried to do something beyond the limitations listed above then it might expose a new security vulnerability in the target server.

To get around this problem the browser first checks with the target server to see whether it will allow the main request. This check takes the form of an HTTP OPTIONS request. Here OPTIONS refers to the request method, it’s one of the alternatives to GET and POST. The request headers of the OPTIONS request describe the main request to the server and then the server responds via the response headers.

An OPTIONS request was chosen for this purpose because most web servers already implemented some form of OPTIONS request handling and such requests should always be harmless to the server.

You don’t have direct control over the preflight request, it’s made automatically by the browser. It will look something like this:

OPTIONS /api-path HTTP/1.1
Origin: http://localhost:8080
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

Other headers will be included but they aren’t important here. Breaking this down line-by-line:

  1. The path /api-path shown here is just an example and will match the URL path of the original request.
  2. The Origin header will match the origin of the page making the request. See What does ‘origin’ mean?. This is exactly the same as the Origin header that will be included on the main request.
  3. The header Access-Control-Request-Method indicates the request method of the main request.
  4. The header Access-Control-Request-Headers is a comma-separated list of custom headers that were set on the request. Headers set by the browser aren’t included, so any headers listed here were set somewhere in the code that attempted the original request. It doesn’t tell us the values of those headers, just that they were set to something other than their default values.

To allow this request, the server response should look like this:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: content-type

Again, breaking this down line-by-line:

  1. The status code must be in the range 200-299 for a preflight request to succeed.
  2. Just like for the main request, Access-Control-Allow-Origin must either match the Origin or be *.
  3. The response header Access-Control-Allow-Methods is a comma-separated list of allowed request methods. GET, POST and HEAD requests are always allowed, even if they aren’t included in the list.
  4. Access-Control-Allow-Headers is also a comma-separated list. For the request to be allowed it must include all of the headers that were listed in Access-Control-Request-Headers.

If any of the CORS response headers are dynamically generated based on the request headers then those request headers should be listed in the Vary response header. e.g. Vary: Origin. This helps to avoid caching problems.

No request body will be sent for a preflight request and the response body will be ignored.

My request works fine in Postman/cURL/etc.. Why do I get a CORS error in the browser?

The same-origin policy is a security feature built into web browsers. It doesn’t apply if you make requests using tools such as Postman or cURL. Concepts such as ‘same-origin’ and ‘cross-origin’ don’t even make sense in that context. Same origin as what? There is no current page making the request, so there is no origin to compare.

Any CORS errors you see in the browser console are not generated by the server. At the network level the request probably succeeded. The browser may well have received exactly the same response that you see with Postman or cURL. It’s the browser that performs the CORS checks and blocks access to the response in JavaScript if those checks fail.

The biggest difference between a browser and tools like Postman is the preflight OPTIONS request. As those tools don’t use CORS they aren’t going to send a preflight request automatically. However, a preflight is just an HTTP request so it is possible to send it manually instead:

Within the browser it’s usually pretty clear from the error message whether it’s the preflight request that’s failing.

How can I use cURL to test a preflight request?

In Chrome or Firefox you should be able to see the preflight OPTIONS request in the Network tab of the developer tools. Right-clicking on the request should present an option to Copy as cURL.

To make the copied request useful you will also need to add the -I option. This includes the response headers in the output.

Most of the request headers included by a browser aren’t necessary from a CORS perspective. Servers will usually ignore those other headers when responding to a preflight request. Generally a much simpler cURL request will suffice:

curl \
  -I \
  -X OPTIONS \
  -H "Origin: http://localhost:8080" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type" \
  http://localhost:3000/api

Breaking this down:

  1. The \ symbols are used to break the command over multiple lines. You can remove them and put the whole thing on one line if you prefer.
  2. As mentioned previously the -I will output the response headers.
  3. The -X is used to set the request method. For a preflight this must be OPTIONS.
  4. -H adds a request header.
  5. The values of the three headers will need changing to match the request you are trying to make. You should omit Access-Control-Request-Headers if there are no custom headers.
  6. The final line is the URL of the target server. Again this is something you will need to change to match the request you are trying to make.

Can Postman send a preflight request?

A preflight request is just an HTTP request, so it can be sent using Postman.

To send the request manually you’ll need to select OPTIONS for the request method and then set suitable values for the headers Origin, Access-Control-Request-Method and Access-Control-Request-Headers.

If you want a preflight request to be generated automatically then you could use Postman’s Pre-request Script feature instead. The code below is an example of how to generate a preflight request for another request. You should be able to drop this code straight into the Pre-request Script tab for your target request:

(function () {
  const request = pm.request
  const url = request.url.toString()
  const requestMethod = request.method
  const headers = request.headers.toObject()
  const origin = headers.origin

  if (!origin) {
    console.log(`The request must have an Origin header to attempt a preflight`)
    return
  }

  delete headers.origin

  const requestHeaders = Object.keys(headers).join(', ')

  if (!['GET', 'HEAD', 'POST'].includes(requestMethod)) {
    console.log(`The request uses ${requestMethod}, so a preflight will be required`)
  } else if (requestHeaders) {
    console.log(`The request has custom headers, so a preflight will be required: ${requestHeaders}`)
  } else {
    console.log(`A preflight may not be required for this request but we'll attempt it anyway`)
  }

  const preflightHeaders = {
    Origin: origin,
    'Access-Control-Request-Method': requestMethod
  }

  if (requestHeaders) {
    preflightHeaders['Access-Control-Request-Headers'] = requestHeaders
  }

  pm.sendRequest({
    url,
    method: 'OPTIONS',
    header: preflightHeaders
  }, (err, response) => {
    if (err) {
      console.log('Error:', err)
      return
    }

    console.log(`Preflight response has status code ${response.code}`)
    console.log(`Relevant preflight response headers:`)

    const corsHeaders = [
      'access-control-allow-origin',
      'access-control-allow-methods',
      'access-control-allow-headers',
      'access-control-allow-credentials',
      'access-control-max-age'
    ]

    response.headers.each(header => {
      if (corsHeaders.includes(header.key.toLowerCase())) {
        console.log(`- ${header}`)
      }
    })
  })
})()

This code requires the original request to have an Origin header set. You can see the results of the preflight in the Postman Console. The code makes no attempt to perform a CORS check on the response headers, you’ll need to verify the response yourself.

Why am I seeing a preflight OPTIONS request when I’m not setting any custom headers?

The first step to debugging an unexpected preflight request is to check the request headers on the preflight request. The headers Access-Control-Request-Method and Access-Control-Request-Headers should clarify why the browser thinks a preflight in required.

First check Access-Control-Request-Method. If it’s set to GET, HEAD or POST then that isn’t the problem. Those 3 request methods are considered safe and won’t trigger a preflight request. For any other values, e.g. PUT or DELETE, that’s enough to trigger a preflight request.

The other header to check is Access-Control-Request-Headers. This will provide a comma-separated list of header names. These are the names of the request headers that triggered the preflight request. It won’t tell you the values, just names. If the preflight succeeds then you’ll be able to find the values on the main request.

Often once you see the names of the headers it’s obvious where they are coming from. But not always. A quick search through your code can help but sometimes even that doesn’t reveal the source of the rogue headers.

If you’re using a library it may be automatically setting headers for you. Some common examples includes:

  • X-Requested-With being set to XMLHttpRequest.
  • Content-Type. The values application/x-www-form-urlencoded, multipart/form-data and text/plain won’t trigger a preflight request but any other value will. Most AJAX libraries will attempt to set the Content-Type header for requests that have a body, such as POST requests. Some libraries will automatically set the Content-Type to application/json when the body contains JSON data. For a cross-origin request that will trigger a preflight.
  • Authorization. If you’re providing options such as username and password then it is likely that the library will be converting them to an Authorization header.

If you still can’t find where the custom header is coming from then you may need to step through the code. Put a breakpoint just before your request and step into the library code to see exactly what is going on.

How can I avoid the preflight OPTIONS request?

Caching

If you want to cut down the number of preflight requests you should consider using caching. Preflight caching is controlled using the response header Access-Control-Max-Age.

Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 3600
Vary: Origin

The Vary header is used to identify which request headers were used to generate the response. The cached response should only be used if the values of those headers are unchanged.

Most browsers will limit the caching time for preflight requests to a few hours, depending on the browser.

The requests must share the same URL to get any benefit from caching. If your URLs are all slightly different, possibly because they include resource ids, then you may want to consider moving those differences into the request body instead.

Completely avoiding a preflight

Avoiding preflight requests is primarily a server-side problem and may require significant architectural changes depending on how your application is structured. Ultimately the server dictates the form of the requests and if those requests require a preflight then there’s nothing the client-side code can do to avoid it.

First you’ll need to limit requests to GET, HEAD or POST. If you’re using other request methods such as PUT or DELETE then you’ll need to change them to POST.

Some custom request headers are allowed without triggering a preflight, though generally they’re not the most useful headers. If you have an existing request that is triggering a preflight then the simplest way to confirm which headers are to blame is to check the value of Access-Control-Request-Headers. See Why am I seeing a preflight OPTIONS request when I’m not setting any custom headers? for more information.

Most custom headers can either be removed or moved into the request body instead.

To avoid a preflight request the Content-Type header must be one of application/x-www-form-urlencoded, multipart/form-data or text/plain. If the body of your POST requests is not in one of these three formats then you’ll need to lie in order to avoid a preflight. If you’re using a third-party library to parse the content then it may need reconfiguring to cope with the misleading Content-Type header. This can be difficult if your server supports multiple types of content. You could use different paths for different formats, or use a URL query parameter to pass the true Content-Type. Either way you may find yourself needing to be inventive to get the parser to understand which requests it should parse.

The Authorization header can also be problematic as it is commonly used by third-party libraries. Whether this is a solvable problem will depend on the library you’re using.

Why am I seeing ‘405 - Method Not Allowed’?

There are a few reasons why you might be seeing this error.

A 405 status code usually indicates that a request is using the wrong request method. e.g. Using POST when it should be GET. That applies to any request, not just CORS requests. Before you do anything else it is worth quickly checking which request method the server is expecting and making sure that you’ve used the correct one when making the request. Check the URL too, make sure you haven’t copied it from somewhere else without updating it.

From a CORS perspective the most likely cause of problems is the preflight OPTIONS request. That’s usually pretty easy to identify as you’ll see an error message in the browser console telling you that the preflight request has failed. Typically the problem is simply that the server hasn’t been configured to support a preflight OPTIONS request. That may be a server bug, or it may be that you’re triggering an unnecessary preflight. See What is a preflight request?. If you have control over the server then also consult the documentation for your server-side stack to check what is required to enable a CORS preflight request.

Next, use the Network tab of the developer tools in your browser to check exactly which request is failing. Pay particularly close attention to the request method, don’t just assume it’s what you wrote in your code. Most browsers allow the columns shown in the Network tab to be configured, so add Method if it isn’t already showing.

If you have the option of checking the server logs then that may also help to provide important clues.

HTTP redirects are also a common source of 405 errors, though not specifically related to CORS. This includes redirecting from http to https or redirects to add or remove trailing slashes. Depending on how this is implemented it can cause the request to change method to GET, causing a 405.

What is withCredentials? How do I enable it?

withCredentials is a flag that can be set on XMLHttpRequest for cross-origin requests. It is usually used to enable cookies. It is also required to enable browser-based HTTP authentication, though that can also be implemented manually.

Note that withCredentials is only required for specific forms of ‘credentials’. Just because you’re using some form of authentication with credentials doesn’t necessarily mean that you need to enable withCredentials.

If you’re using XMLHttpRequest directly it would be set as follows:

const httpRequest = new XMLHttpRequest();
httpRequest.withCredentials = true;

It can be set at any point prior to calling httpRequest.send().

For making requests with fetch the equivalent of withCredentials is setting credentials to 'include':

fetch(url, {
  credentials: 'include'
});

With jQuery the withCredentials flag can be set using:

jQuery.ajax({
  // ...other settings...

  xhrFields: {
    withCredentials: true
  }
});

For axios:

axios.post(url, body, {
  withCredentials: true
});

Note that withCredentials is not a header and should be included directly in the request options.

The use of withCredentials is not a factor in determining whether a preflight request is required. Even though withCredentials can lead to the automatic inclusion of Cookie and Authorization headers on the request they are not considered to be custom headers for the purposes of the preflight check.

When using withCredentials the server response must include the header Access-Control-Allow-Credentials: true, otherwise the request will fail the CORS check. This header must also be included on the preflight response, if there is one.

The use of * values in CORS response headers is also prohibited when using withCredentials. For Access-Control-Allow-Origin the value of the Origin request header should be used instead but only after it has been checked to ensure the origin can be trusted. See What are the security implications of CORS? for more information about why this matters.

While Safari does support withCredentials it tends to have a stricter security policy than other browsers. If you need to use withCredentials then you should test in Safari sooner rather than later to check whether what you’re trying to do is actually allowed. For example, to set cookies you will need both origins to share the same domain.

What happens when a CORS request fails?

The most obvious sign that a request has failed due to CORS is an error message in the browser console. This will usually give a clear indication of why it failed. For more information about CORS error messages in Chrome see our list of CORS error messages.

It’s important to appreciate that CORS error messages come from the browser, not from the server. The browser applies the CORS checks to the response headers after a response is successfully received.

A notable exception is the message Reason: CORS request did not succeed, which is shown in Firefox. If you just see that message then it is possible that the request failed at the network level. For example, you will see that message if the target server couldn’t be contacted. The message is somewhat misleading as CORS is not really relevant to the problem. The equivalent message in Chrome doesn’t mention CORS and is the same message that would be shown for a same-origin request.

The Network section of the browser’s developer tools won’t tell you directly whether the request failed a CORS check, though that is usually the easiest way to check the relevant headers.

If a CORS preflight OPTIONS request fails then the main request won’t occur.

In some browsers the preflight request won’t be shown separately in the developer tools. Any console errors should make it clear whether it was the preflight request that failed.

In your JavaScript code all CORS failures will be presented the same way. You’ll see a status code of 0 and you won’t be able to access the response headers or the response body. You won’t have access to any helpful error messages as exposing those error messages is regarded as a security risk.

In some cases cookies will be set even though the request failed a CORS check. However, unless the cookie domain is a match for the current page you won’t be able to access those cookies via document.cookie.

A preflight OPTIONS request is expected to return a status code in the range 200 to 299, otherwise it will fail. However, the main CORS request can use status codes just like any other AJAX request. A status code that indicates an error will not cause the CORS checks to fail and, if the CORS checks pass, that status code will be accessible in your JavaScript code.

Why is CORS so complicated?

How complicated you find CORS depends on your starting point.

If you’re expecting cross-origin requests to be exactly the same as same-origin requests then it will definitely seem complicated. Unfortunately, the same-origin policy is very much needed, it is not just browser makers worrying about nothing.

It’s quite likely that your first encounter with CORS was an error message in your browser’s console. Blissfully ignorant of its significance you probably expected that there’d be a nice, easy fix.

The good news is that CORS does provide a solution. Prior to CORS being introduced you’d have been in real trouble, fumbling with JSON-P for a bit before giving up and throwing your whole project in the bin.

The bad news is that CORS had to be shoe-horned into the existing design of the web without breaking anything or introducing significant new security problems.

If you aren’t really familiar with the inner workings of HTTP then CORS may be dragging you into unknown territory. HTTP headers and OPTIONS requests nicely solve the problem but if you haven’t come across the basic concepts before then they make the CORS learning curve seem a lot steeper than it actually is.

There are several factors that contributed extra complexity to the design of CORS:

  • Security must be backwards compatible with servers that don’t understand CORS.
  • Special cases have been added to make simple scenarios easier. However, if you want to understand the full picture then those special cases are just extra stuff to learn.
  • There was a desire to support browser caching, especially for the preflight request. This is one reason why the preflight response requires multiple headers and not just a simple yes-or-no header.

If you’re new to CORS and getting a bit overwhelmed then it may help to reset your expectations. Be patient and allow yourself some time to find out how CORS works. If you try to rush it you’ll just get lost.

Remember that CORS exists because of very real security concerns. You need to make an informed decision about exactly how much of that security you want to turn off.

CORS is annoying. Why can’t I turn it off?

Some browsers have command-line options or similar settings to turn off the cross-origin security restrictions. The details are not included here because this is not something you should be doing, even during development. If you do need a temporary workaround for development see What are the alternatives to CORS?.

If the web were to be redesigned from scratch it might look very different. But that isn’t going to happen. Instead browsers have to do the best they can with the web we have today.

Web browsers have to protect their users from malicious sites. You may know that your site isn’t malicious but a browser doesn’t know that.

For a cross-origin request the browser is trying to protect the other server from your site. It assumes that your site could be malicious, so it wouldn’t make sense to allow your site to disabled the security protection.

Perhaps you control both servers. As far as you’re concerned they’re both part of the same site. But the browser doesn’t know that, it just sees two different origins (servers) and has to treat them as totally separate.

Before CORS existed the same-origin policy just blocked cross-origin AJAX requests. Now that was really annoying. At least with CORS the server can choose to allow the request.

What are the alternatives to CORS?

Before CORS existed there was JSON-P. Now that browser support for CORS is universal there’s no good reason to use JSON-P instead.

If you just need a solution locally during development then you could try using a browser extension. Typically these extensions will intercept the server response and inject CORS headers. This may be sufficient in some cases but it’s not ideal as the requests are still made cross-origin, leading to potential problems, e.g. with cookies.

If you’re trying to contact a publicly accessible server then you could try using CORS Anywhere, cors-anywhere.herokuapp.com. It acts as a middle-man, adding in the required headers to get CORS to work. For experiments or demos this might be a satisfactory solution but it’s not a good idea to use it for a production application.

The other alternative is to use a reverse proxy to channel all requests through a single server. This may be a viable solution both during development and in production. Cross-origin restrictions don’t apply if the requests all target the same origin as the current page. The server needs to be configured to pass on relevant requests to the other server. When the response shows up, it passes that back to the browser. As far as the browser is concerned it’s just talking to one site.

This might be a good solution but there are some drawbacks to consider:

  • Many hosting solutions will not allow you to configure proxying.
  • As the AJAX requests are now going through your server the load on that server will increase. The network infrastructure between the two servers will also have to cope with the extra demand.
  • The total request time will increase.
  • If you are proxying someone else’s site you might be violating the terms and conditions of that site.
  • The other site will see all requests as having come from your IP address. If you make too many requests you may be throttled or even blocked.
  • If sensitive data is being transferred then you are now responsible for protecting that data while it passes through your server.

I can’t change the server and it isn’t using CORS. What else can I do?

See the previous question: What are the alternatives to CORS?.

Does using HTTPS have any effect on CORS?

Yes.

Requests from HTTPS

If the page making the request is using HTTPS then the target URL must also be HTTPS. Trying to access resources using http from an https page is known as mixed content and will be blocked the browser. It won’t even attempt the request and there should be a clear error message in the console.

Most browsers relax the rules for CORS requests to localhost and 127.0.0.1, so it is possible to make requests locally using http. This is considered lower risk as the request never actually leaves the user’s device. Safari currently doesn’t implement this special case and will block any request from https to http.

Requests to HTTPS

If the requesting page has a scheme of http then it can make CORS requests to both http and https URLs.

Invalid SSL certificates, especially self-signed certificates, are a common problem when using CORS. These can result in requests failing for no apparent reason. For a same-origin request the certificate of the requesting page will be the same as the requested URL, so any problems with the certificate will have been handled as soon as the page was opened. For a CORS request the certificate won’t be checked until the request is made, so it fails quietly in the background.

Usually the easiest way to check the certificate is to go directly to the URL in a browser. Even though this won’t be using CORS it will still be sufficient to check that the certificate is valid. With a self-signed certificate this will allow you to add an exception so that the certificate will be trusted in future.

Cookies

As of Chrome 80, cookies with SameSite=None must also set the Secure directive. So if you need cross-domain cookies you’ll need to use HTTPS.

Note that a cookie’s domain is not quite the same thing as origin, so it is possible to have cross-origin cookies without HTTPS if the domains match.

For more information see Why aren’t my cookies working with CORS?.

Does CORS work with localhost?

Yes. From a CORS perspective localhost and 127.0.0.1 are almost the same as any other domain or IP address.

The server you are attempting to contact may choose to allow requests only from specific origins. CORS itself doesn’t make a special case for localhost but a server can single out localhost if it so wishes.

Typically localhost is only used during development. This can lead to a perception that localhost is somehow the cause of a CORS problem. In reality it’s a case of correlation not implying causation. Some more likely causes are:

  1. A bug in the code or CORS configuration.
  2. Caching, making a problem appear to stick around even after it is fixed.
  3. An invalid or self-signed SSL certificate.
  4. Not binding the target server to localhost. Try contacting the server directly to be sure it is accessible via localhost.
  5. A browser plugin, debugging proxy or some other piece of development trickery.

Far from localhost having tighter CORS restrictions, in some cases it actually has weaker restrictions (see HTTPS and Cookies below). This can cause problems in production that didn’t occur during development. To avoid such problems you may want to consider adding aliases to your hosts file so that you can use URLs during development that are a closer match to the production URLs.

HTTPS

There is a special case in some browsers for mixed content. If an https page attempts a request to an http page then this is usually blocked. However, if the target page is using localhost then a CORS request is attempted.

See Does using HTTPS have any effect on CORS? for more information but, in short, if you want to use https for the requesting page you’ll also need to use https for the target server.

Cookies

Consider this relatively common scenario.

During development you might be running both servers on localhost. Let’s say the UI is hosted at http://localhost:8080 with a data server at http://localhost:3000.

You open http://localhost:8080 in your web browser and you’re presented with a login page. You enter your username and password and the page sends a login request to http://localhost:3000. This returns a cookie using the Set-Cookie header. The request had withCredentials set to true and the cookie seems to work as expected.

When this site reaches production the UI is hosted from http://www.example.com and the data server is at http://api.example.com. Suddenly the cookies stop working.

The problem is that cookies are tied to a domain. Working locally both localhost:8080 and localhost:3000 are considered to have a cookie domain of localhost. So even though it was a cross-origin request, from a cookie perspective it’s considered ‘same site’.

For the production site the cookie’s domain would default to api.example.com, which is not a match for www.example.com. As they are both subdomains of example.com this can easily be fixed by explicitly setting the Domain directive on the cookie. However, the key point to note is that production behaves differently from the development environment.

If you want to know more about working with cookies and CORS see Why aren’t my cookies working with CORS?.

I’ve tried to implement CORS but it isn’t working. What should I do next?

First make sure you’ve understood how CORS works. If you aren’t clear on that then you’ll waste a lot of time trying to debug any problems.

There are a lot of third-party libraries available that implement CORS for various different types of server. These can save you some of the work but it is still important to understand how the underlying CORS mechanism works or you’ll likely run into problems.

If CORS has failed you’ll probably see an error message in your browser’s console. If you don’t see an error message then check that you don’t have any filters turned on that might be hiding the message.

If you’re seeing a CORS-related error message but you aren’t sure what it means then try consulting our list of CORS error messages.

The next thing to try is the Network tab of the developer tools. Find the request that isn’t working and check exactly what is being sent each way.

If you’re seeing an OPTIONS request that you weren’t expecting then see Why am I seeing an OPTIONS request instead of the GET/POST/etc. request I wanted?.

If you’re seeing the warning ‘Provisional headers are shown’ then see Why can’t I access the response headers in Chrome’s developer tools?.

If you think all the headers look correct then you can check them using our CORS header checker.

I’ve configured my server to include CORS headers but they still aren’t showing up. Why?

First check for any error messages in the server logs.

If that doesn’t help, here are some common problems to check:

  • Have you saved the config file?
  • Is the config file in the right place?
  • If you have multiple servers, have you changed the correct one?
  • The server may need restarting. Make sure you restart the correct server.
  • CORS may be configured for some requests but not the request you’re attempting.
  • Is the failing request a preflight OPTIONS request? Does your configuration handle that?
  • Could a proxy or intermediary server be removing the headers?
  • Check for typos in your config. e.g. Spelling (allow / allowed), plural vs singular (headers / header), case-sensitivity.
  • If you’re using an online tutorial, is it for a compatible version of the server and/or CORS plugin that you’re using?

One trick that can be useful is to try changing something unrelated in the config file, see whether that works. The idea is to confirm that the latest version of the config file is definitely being used. Even deliberately breaking the config file so that the server won’t start is enough to confirm that your changes are having an effect.

Why aren’t my cookies working with CORS?

There are several questions in one here:

  1. How can cookies be set using the Set-Cookie response header using CORS?
  2. Why can’t I see my cookies in the developer tools?. This is so common it gets a separate question in the FAQ.
  3. Can CORS cookies be accessed from JavaScript using document.cookie?
  4. I’ve set the cookies but they aren’t being included on subsequent CORS requests. Why?

There’s a complication that warrants mentioning up front. Cookies are bound to a domain and path, not an origin. So we’ve actually got two slightly different concepts of ‘same site’ to juggle. Fun times.

Even if you aren’t using CORS a cookie can disappear because one of its directives is incorrectly set. As that isn’t relevant to CORS we aren’t going to go into detail here but you should check that directives such as Expires, Max-Age, Domain, Secure, etc. aren’t set to inappropriate values.

Now let’s consider the case where the domains/origins are totally different. We’ll come back to the muddy waters in the middle later.

If you’re using XMLHttpRequest to make a CORS request then you’ll need to set the withCredentials flag to true. For fetch the equivalent setting is credentials: 'include'. For more information on that see What is withCredentials? How do I enable it?.

Once you’ve set this flag you’ll likely see a number of errors and warnings in your browser’s console. What follows below is mostly just an explanation of how to fix those errors.

On the server, as well as returning the Set-Cookie and Access-Control-Allow-Origin headers, you’ll also need to return an extra CORS header to allow credentials:

Access-Control-Allow-Credentials: true

If the request requires a preflight then that must also include this header.

Using credentials disables the * wildcard for the other CORS response headers, so if you’re using that you’ll need to replace it with explicit values. The most common problems are with Access-Control-Allow-Origin, which will need to return the exact value of the Origin request header instead of *.

Then there’s the SameSite directive of Set-Cookie to consider. For cross-domain requests it needs to be set to None or the cookie will be ignored. Note that cross-domain isn’t quite the same thing as cross-origin, we’ll elaborate on that distinction shortly. In most browsers None is the default value but as of Chrome 80 this is changing, www.chromium.org.

From February 2020 Chrome will be transitioning the default value to Lax, so SameSite=None will need to be set explicitly.

As part of the same transition, Chrome will also require that cookies using SameSite=None also use the Secure directive, which requires https. So if you want to use cross-domain cookies you’re going to need https.

Putting all those headers together we get something like this:

Access-Control-Allow-Origin: example.com
Access-Control-Allow-Credentials: true
Set-Cookie: my-cookie=value; SameSite=None; Secure

Even if you do all this the cookie still won’t be set in Safari, which has tighter security restrictions than other browsers. There is a workaround though…

At this point we need to go back to those muddy waters around origins and cookie domains.

Let’s consider a website running at http://localhost:8080 making AJAX requests to http://localhost:3000. The ports don’t match so they have different origins. We’re in CORS territory.

However, a cookie domain is not the same thing as an origin. Cookies for both of these server will have a domain of localhost. The port is ignored. So from a cookie-domain perspective they count as the same site.

Keep in mind that cookies were introduced to the web a long time ago. If they were introduced from scratch today they would likely be designed very differently.

So continuing with our example website running at http://localhost:8080, it has the same cookie domain as http://localhost:3000. They share a cookie jar. In JavaScript code the cookies will be accessible via document.cookie, no matter which of the two servers set a particular cookie. The SameSite directive can be set to None, Lax or Strict - it doesn’t matter because from a cookie perspective they count as the same site. You’ll still need to use Secure if you want SameSite=None with newer browsers but if both of your servers share a domain you probably don’t want to be using SameSite=None anyway.

Using a shared cookie domain isn’t limited to localhost but it is a little more complicated once subdomains get involved. If you have a website running at http://www.example.com making AJAX requests to http://api.example.com then they won’t share cookies by default. However, a cookie can be shared by explicitly setting the Domain to example.com:

Set-Cookie: my-cookie=value; Domain=example.com

Even Safari will allow cross-origin cookies to be set so long as they share a cookie domain.

If you’ve read all that and still can’t figure out why your cookies aren’t being set, try using our CORS header checker to check that you’re setting the response headers correctly. Also take a look at Why can’t I see my cookies in the developer tools?.

A cookie set via a CORS request can be accessed in JavaScript via document.cookie but only if the cookie’s domain is a match for the current page. Whether a CORS request was used to set the cookie is not actually relevant.

Let’s assume that you’ve successfully managed to set a cookie for the correct domain. How do you include that on subsequent CORS requests to that domain?

The process is quite similar to setting a cookie using CORS. The withCredentials flag must be set to true and the server will need to return Access-Control-Allow-Credentials: true. As before, wildcards won’t be supported for any CORS response headers.

Cookies with a SameSite value of Strict or Lax will only be sent if the page domain matches the cookie domain. If the domains don’t match then SameSite must be None. This is consistent with setting a cookie using CORS so it will only be a problem if the cookie was set by some other means.

While Safari has tighter restrictions for setting cookies, the rules for including cookies on subsequent requests are much the same as for other browsers. So while a same-domain request may be required to set the cookie, it can then be included on a cross-domain request from a different page.

Why can’t I see my cookies in the developer tools?

This is a common misunderstanding.

The developer tools will only show cookies for the current page. Cookies for cross-origin AJAX requests are usually not regarded as being part of the current page, so they aren’t shown.

Depending on the specifics of your scenario you may see the cookies in the developer tools or you may not. For example, if you’re running two servers on localhost with different ports then they will share a cookie domain, so the cookies should show up.

Just because the cookies aren’t shown in the developer tools doesn’t mean that they don’t exist.

To see the cross-origin cookies in the developer tools you’ll need to open another tab with a URL that has the same domain as the cookie. It doesn’t matter exactly which URL you choose but you should be careful to pick a URL that won’t change the cookies itself. You’ll also need to open a separate copy of the developer tools for the new tab. You should then be able to see what cookies are set for that origin.

Alternatively, most browsers provide some mechanism for viewing all cookies. It’s usually hiding somewhere in the privacy settings. At the time of writing the following URIs will get you to the right place:

  • Chrome: chrome://settings/siteData
  • Firefox: about:preferences#privacy

Of course, the other reason why you may not be able to see the cookies in the developer tools is because no cookies are being set. See Why aren’t my cookies working with CORS? for more information.

Why can’t I access the response headers in my JavaScript code?

By default, only the following response headers are exposed to JavaScript code for a CORS request:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

These are known as the CORS-safelisted response headers.

The specification was recently changed to add Content-Length to the list of CORS-safelisted response headers. This has been implemented in some browsers but at the time of writing it still isn’t included in Firefox.

To expose other response headers you need to use Access-Control-Expose-Headers. See developer.mozilla.org for more information.

Which CORS response headers go on the preflight response and which go on the main response?

These two headers should be included on both the preflight and the main response:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Credentials

The following headers should only be included on the preflight response:

  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Max-Age

The Access-Control-Expose-Headers header should only be included on the main response, not the preflight.

Most of these response headers are optional, depending on the circumstances. The only header that is always required for a CORS request to succeed is Access-Control-Allow-Origin. For a preflight request, at least one of Access-Control-Allow-Methods or Access-Control-Allow-Headers will also be required.

Why can’t I see my request in the Network section of the developer tools?

First and foremost check for console errors. The simplest explanation for a missing request is that there’s a bug in your code and the request didn’t actually happen.

One particularly noteworthy error shown in Chrome is:

Access to XMLHttpRequest at 'localhost:8080/api' from origin 'http://localhost:3000' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

Interestingly, the same problem in Firefox doesn’t show any error message, the request just fails without explanation.

The source of this problem is that the URI is missing the prefix http:// or https://, so the browser interprets the localhost: as being the scheme of the URI. More information.

However, assuming you aren’t seeing that particular error…

In most browsers the developer tools must be open before you make the request, otherwise it won’t be recorded.

Check you don’t have any filters turned on which could be hiding your request.

In Safari and older versions of Chrome you won’t be able to see the preflight OPTIONS request as a separate request.

Preflight OPTIONS requests can also be cached, so it’s possible you may not see the request you’re expecting because a cached response has been used instead. If you have access to the server logs you may be able to confirm what requests were actually received.

It can be useful to check across multiple browsers. Using a new Incognito/Private window can also help to bypass any caching problems that might be interfering with your requests.

Why can’t I access the response headers in Chrome’s developer tools?

When you try to access the headers via the Network tab of Chrome’s developer tools you may see the warning:

Provisional headers are shown

Further, the context-menu for the request won’t show the options to Copy request headers or Copy response headers.

There are several possible causes for this warning, some of which are not directly related to CORS. It may just mean that the request hasn’t finished yet because the server is taking a long time to respond. It’ll also be shown if you try to access files directly off the file-system without using a web-server.

However, the most likely cause for a CORS request is that the preflight checks are failing. If the preflight OPTIONS request fails then the main request won’t even be attempted, so there are no headers to show. There should be a corresponding error message in the browser’s console.

How do the Authorization and WWW-Authenticate headers work with CORS?

The Authorization request header can be set as a custom header on a request and, just like any other custom header, it would trigger a preflight OPTIONS request.

However, a bit like the Cookie header, the Authorization header can also be included automatically by the browser as part of HTTP authentication. Ignoring the cross-origin aspect for a moment, the steps for this form of authentication are:

  • The browser requests a URL.
  • The server responds with a 401 status code and a WWW-Authenticate header indicating the authentication scheme to be used.
  • The browser prompts the user for credentials.
  • The original request is retried with the credentials encoded into the Authorization header.
  • Subsequent requests to the same URL will automatically included the Authorization header.

For cross-origin requests the withCredentials flag must be set to true. In most browsers that should enable HTTP authentication, the exception being Safari.

If the withCredentials flag is not set, or the user does not provide credentials, then the original 401 response will be the response exposed via JavaScript.

If a preflight OPTIONS request is required then this must succeed without any authentication.

Oddly, the 401 response does not need to include the Access-Control-Allow-Credentials header for the browser to prompt for credentials. However, it should be included anyway in case the user declines the credentials prompt. In that scenario the 401 becomes the exposed response and then Access-Control-Allow-Credentials is required.

How can I include authorization headers on the preflight OPTIONS request?

You can’t. The server must be configured to respond to the preflight OPTIONS request without any authentication headers being present.

If you’re using a filter or middleware layer on the server to block all unauthorized requests then you’ll need to provide an exception for the preflight OPTIONS requests.

Does CORS support HTTP redirects?

Yes. CORS requests do support HTTP redirects so long as all the requests include the relevant CORS response headers.

When we say ‘HTTP redirects’ we mean using status codes such as 301, 302, 307 or 308 in conjunction with a Location response header. For a typical AJAX request these redirects are performed automatically by the browser without any explicit handling in client-side JavaScript.

The specification for CORS has gradually become more permissive towards redirects. The information presented here reflects what is currently implemented in browsers but it is likely to continue to change.

A preflight OPTIONS request must not attempt a redirect. Instead the preflight should just return the usual response headers required for the CORS checks to pass. The redirect can then be performed on the main request.

A potentially problematic scenario occurs if the redirect is to a URL with a different origin from the URL that was originally requested. This is allowed but when the browser attempts the new request it will set the Origin header to null. This is not a bug, it is a security precaution included by design.

Even if you aren’t intentionally using redirects there are two common ways that they can creep in:

  1. Redirecting from http to https.
  2. Redirecting to add or remove a trailing URL slash. e.g. A server may redirect http://example.com/api/users to http://example.com/api/users/ or vice-versa.

If not done correctly this can change the request method to GET, which can trigger other errors such as a 405 response from the server. Inadvertently changing to a GET request will also cause the request body to be dropped.

Often the simplest solution is to use the final URL instead and skip the redirect altogether.

Why is my Origin ‘null’?

Under some circumstances the request header Origin can be set to the special value null.

Origin: null

Note that this is the 4-character string "null", not to be confused with the null keyword used by many programming languages.

Within the browser the value of location.origin can also be the string "null".

This special value is used whenever a proper origin value doesn’t exist or can’t be exposed for security reasons. The request header may be null even if location.origin has a proper value.

Some examples:

  • If the page is being loaded directly off the file-system using the file: scheme, without a web-server, it is still allowed to make HTTP requests but the Origin header will be null.
  • Likewise, a page created using the data: scheme will have a null origin. e.g. <iframe src="data:text/html,...">. Here the ... would be the URI-encoded contents of a web page to show in the iframe. The web page inside the iframe would have a null origin.
  • An iframe using sandboxing, such as <iframe src="..." sandbox="allow-scripts">. Within the iframe the value of location.origin may be populated based on the src URL but any CORS requests will have a null origin.
  • An HTTP redirect on a CORS request that changes the target origin. Even if the original request had a proper Origin header the redirected request will have Origin: null.

It is still possible for null to pass a CORS check, just like for any other Origin value:

Access-Control-Allow-Origin: null

It has been suggested that the specification should be changed to prevent null matching itself, so it is possible this may stop working in future. As there are many different ways for Origin to be null it is quite difficult to target a specific case on the server. The Referer header may still be available in some cases as a hint to what the Origin would have been but that isn’t reliable either. Generally it is recommended not to allow access from null origins explicitly, though Access-Control-Allow-Origin: * can be used for genuinely open resources.

What are the security implications of CORS?

It’s almost impossible to provide a comprehensive list but here are some of the common concerns.

Can’t someone send the request via cURL to bypass the preflight or spoof the Origin header?

Yes, they can.

This kind of attack has always been possible, even with servers that don’t use CORS. The defence against these attacks is typically two-fold:

  1. Authentication and authorization checks to ensure the user sending the request is allowed to make the request.
  2. Validation and/or sanitization of all the data on the request to ensure it’s in an acceptable form.

Relying on a UI or web browser to perform these checks isn’t sufficient, they need to be on the server.

So what’s the point of CORS if it can easily be bypassed?

CORS aims to stop someone making a request while pretending to be someone else.

Let’s say you open a webpage in your browser. The page you open is malicious: someone has put some JavaScript code into the page that is trying to cause trouble. It fires off some HTTP requests to other websites pretending to be you. There are two main varieties of mischief that it may try to inflict:

  1. Stealing data. e.g. It might send a request to your webmail and grab a copy of your emails.
  2. Changing data. e.g. Deleting the contents of your database or transferring money from your bank account or buying something on your behalf from an eCommerce site.

The Access-Control-Allow-Origin response header is primarily concerned with the first problem, stealing data. At the network level the data is still transferred but if the Access-Control-Allow-Origin header doesn’t allow the current origin then the malicious script can’t read the response.

For the second problem CORS has preflight requests. The potentially harmful request won’t even be attempted unless the preflight allows it.

It is important to appreciate that a ‘malicious site’ may not have started out as malicious. You may have even created it yourself. The problem is XSS vulnerabilities, which allow hackers to inject their own code into the site. When you enable CORS to allow requests from other sites you aren’t just trusting the sites’ developers not to be malicious, you’re also trusting them not to have any XSS vulnerabilities that leave your server exposed.

Hmmm. That raises more questions than it answers. For starters, how does this malicious site pretend to be me?

There are several options here.

The most obvious answer is cookies. If you’ve logged into a site and it uses cookies to identify you then those cookies will be included by the browser on all requests to that site. The malicious script doesn’t need direct access to the cookies, it just makes a request and the browser includes the cookie automatically.

This type of browser magic falls under the heading of ambient authority. The withCredentials flag is used to control three types of ambient authority:

  1. Cookies.
  2. The Authorization header as part of HTTP authentication. This shouldn’t be confused with using the Authorization header explicitly as a custom request header, which is not ambient authority. See How do the Authorization and WWW-Authenticate headers work with CORS? for more information.
  3. TLS client certificates.

These forms of ambient authority could have been left out of CORS altogether and some initial implementations didn’t allow them. However, enough developers wanted cookie support that the current compromise was eventually included.

If you’re using cookies and don’t need to support cross-origin requests then you should consider setting the directive SameSite to either Strict or Lax. Browsers are gradually switching to Lax by default, away from the historical default of None, but you don’t need to wait if you set it explicitly.

So if it wasn’t for cookies we wouldn’t need any of this response header nonsense?

There are other forms of ambient authority that are less easy to avoid and which pose very real problems to the design of CORS.

For example, a site could use IP addresses or network layout to prevent unauthorized access.

A common scenario is a site hosted on an internal network that allows access to anyone on that network. The ‘security’ here assumes that the site isn’t accessible outside the local network. An external hacker can’t send HTTP requests directly to the server. However, if someone on the internal network opens the hacker’s malicious site then it can start sending requests to those internal sites from within the browser. The page running in the browser is being used as a bridge between the internal network and the outside world.

Router configuration pages are a particularly common example. Chances are your home internet connection includes a router with a webpage to configure your home network.

For the ‘stealing data’ problem, why not just return an empty response instead?

For a new server you could do precisely that. However, CORS had to be designed to work with servers that already existed and had no knowledge of CORS. Those servers won’t have the relevant response headers so the browser will prevent access to the response.

The response in that scenario still makes it to the browser and would be accessible in the developer tools. That isn’t a problem as the developer tools are only accessible to the person using the device. CORS isn’t trying to protect the data from that person. Quite the opposite, that person is the potential victim of the data theft. CORS is trying to stop a malicious script embedded in the page from accessing the response and passing it on to someone else.

Not all requests use a preflight. Doesn’t this leave the door wide open to the hackers in cases where they don’t need access to the response?

Yes, it does.

However…

It is a door that was already open before CORS was introduced. This particular vulnerability goes by the name CSRF (or XSRF), which stands for cross-site request forgery.

Historically a CSRF attack could be performed in various ways but the most interesting is probably an HTML <form>. Such a form could be submitted via a POST request from the malicious site to pretty much anywhere. The same-origin policy did not prevent form submissions, it just prevented the source page from accessing the response.

Roughly speaking, the requests that don’t need a preflight are the same requests you could make using a <form> instead.

While this is a security hole, it’s a hole that has existed for a long time and techniques have been developed to protect against it. CORS just tries not to make the hole any bigger.

The withCredentials flag doesn’t trigger a preflight. Wouldn’t it be safer to always use a preflight check with cookies?

It would. But, again, CORS isn’t introducing any new security holes here. It’s just retaining the holes that already existed with HTML forms.

The gradual shift by browsers towards defaulting to SameSite=Lax should help to protect cookies from CSRF abuse going forward.

Why is a * value for Access-Control-Allow-Origin not allowed when using withCredentials?

The reasoning goes something like this:

  1. A * value exposes the content to any other webpage that wants it. This includes potentially malicious pages.
  2. If the content is always the same, no matter who requests it, then exposing it to everyone isn’t necessarily a problem.
  3. However, if the request requires withCredentials to be set then the content isn’t the same for everyone. Either access is restricted or the content varies by user. In this scenario the malicious page is now in a position to steal the version of the content that’s accessible to the current user.

But can’t I just avoid the limitations on * by echoing back the Origin header in Access-Control-Allow-Origin instead?

Unfortunately, yes you can. Some libraries will even do this for you.

This is just as bad as using *. The only reason CORS doesn’t prevent it is because it can’t. There’s no way for the browser to know that your server is indiscriminately echoing back the Origin header.

To be clear, there’s nothing wrong with echoing back the Origin header for specific, trusted origins. That’s precisely how CORS is supposed to work. The problems arise when there aren’t adequate restrictions on the origins that are allowed.

If you’re returning Access-Control-Allow-Credentials: true then you shouldn’t be echoing back all origins in Access-Control-Allow-Origin. Chances are you have a gaping security hole. Worse, as discussed earlier, hosting your site behind a firewall on an internal network is unlikely to protect you.

If I can’t use *, is there a way to allow all subdomains, e.g. *.example.com?

The CORS specification doesn’t allow for this. Only the exact value * is special and it can’t be used as a wildcard in other values.

Configuring a server to echo back all origins for a particular domain can be quite tricky to get right. Consider the following examples of origins:

http://example.com
https://www.example.com
http://evil-site-example.com
http://example.com.evil-site.com

We might want to support the first two origins, including http, https and all subdomains, but without matching the other two. Those other two origins have example.com as a substring but they are totally unrelated domains that could be under the control of anybody. Further, configuration based on regular expressions needs to be careful to escape the . character to avoid it being treated as a wildcard.

You might think that no-one is going to bother attacking your site because it’s small and not worth the effort. Unfortunately these exploits can easily be found just by using scripts that trawl the web looking for vulnerable sites. These hackers (usually script kiddies) aren’t trying to attack your site specifically, they just set the script running and wait for it to find a victim.

There’s also a problem with the premise of this question. You probably shouldn’t be trying to allow access to all subdomains in the first place. If it isn’t possible to list all the relevant subdomains explicitly then it probably isn’t safe to trust them all either. If any subdomain is running an application with an XSS vulnerability then it could potentially be compromised.

I read somewhere that Access-Control-Allow-Origin: null is potentially insecure. Why?

If you aren’t familiar with the special origin value null then see Why is my Origin ‘null’?.

Part of the problem is that some developers mistakenly believe that returning null is equivalent to omitting the header altogether. A bit of basic testing may even seem to confirm that.

The reality is that returning Access-Control-Allow-Origin: null will allow any request with Origin: null.

Generally you can’t set the Origin header in your client-side code, the browser will set it for you. However, it’s relatively easy to use iframes or HTTP redirects to coerce the browser into sending Origin: null. So if you allow requests from null you’re effectively allowing them from anywhere.

As we’ve already discussed, allowing requests from anywhere is fine under certain circumstances. However, in those circumstances you can just use Access-Control-Allow-Origin: * instead.

Where can I read more about the security implications of CORS?

You might find these useful:

What is an opaque response?

If you’re using the fetch API then you might have come across this message in Chrome:

If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

It’s shown at the end of some CORS error messages.

The first thing to appreciate is that disabling CORS does not mean disabling the same-origin policy. It is important to be clear about the difference between the two.

An opaque response is an HTTP response where you can’t access any of the details in your JavaScript code. It is opaque in the sense that you can’t look into it. That includes the status code, the headers and the body content. You may notice that it’s very similar to what happens when a response fails the CORS checks.

To make a fetch request with an opaque response set the mode to 'no-cors':

fetch(url, {
  mode: 'no-cors'
}).then(response => {
  console.log(response.type) // logs the string 'opaque'
})

Some notes:

  • The mode is only relevant for a cross-origin request, it doesn’t matter for same-origin requests.
  • Any CORS response headers will be ignored. Even if they are included you won’t be able to read the response.
  • A GET request won’t include the Origin request header. It will still be included for POST requests, just like it would for same-origin requests.
  • Only simple requests that do not require a preflight are allowed.
  • There is no equivalent if you’re using XMLHttpRequest.

A request made using mode: 'no-cors' won’t undergo CORS checks in the browser, so the usual CORS error messages won’t be shown. But other than suppressing the error messages, what use is it?

In practice the use cases are pretty limited, so if you’re seeing the error message mentioned earlier it is unlikely to be the solution you want.

One use case is for requests where you handle success and failure exactly the same way. You try to tell the server to do something but whether or not it succeeds doesn’t have any impact on the UI.

Another use case is caching. The requests can be used to pre-populate caches for things like stylesheets where you don’t need to access the response details in JavaScript code.

You can suggest improvements to this page via GitHub.