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:
- Are you allowed to make the request at all?
- 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
andPOST
requests. - There was no way to set custom request headers.
- A
POST
request could only have acontent-type
ofapplication/x-www-form-urlencoded
,multipart/form-data
ortext/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:
- The path
/api-path
shown here is just an example and will match the URL path of the original request. - The
Origin
header will match the origin of the page making the request. See What does ‘origin’ mean?. This is exactly the same as theOrigin
header that will be included on the main request. - The header
Access-Control-Request-Method
indicates the request method of the main request. - 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:
- The status code must be in the range
200
-299
for a preflight request to succeed. - Just like for the main request,
Access-Control-Allow-Origin
must either match theOrigin
or be*
. - The response header
Access-Control-Allow-Methods
is a comma-separated list of allowed request methods.GET
,POST
andHEAD
requests are always allowed, even if they aren’t included in the list. 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 inAccess-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:
- 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. - As mentioned previously the
-I
will output the response headers. - The
-X
is used to set the request method. For a preflight this must beOPTIONS
. -H
adds a request header.- 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. - 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 toXMLHttpRequest
.Content-Type
. The valuesapplication/x-www-form-urlencoded
,multipart/form-data
andtext/plain
won’t trigger a preflight request but any other value will. Most AJAX libraries will attempt to set theContent-Type
header for requests that have a body, such asPOST
requests. Some libraries will automatically set theContent-Type
toapplication/json
when the body contains JSON data. For a cross-origin request that will trigger a preflight.Authorization
. If you’re providing options such asusername
andpassword
then it is likely that the library will be converting them to anAuthorization
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:
- A bug in the code or CORS configuration.
- Caching, making a problem appear to stick around even after it is fixed.
- An invalid or self-signed SSL certificate.
- Not binding the target server to
localhost
. Try contacting the server directly to be sure it is accessible vialocalhost
. - 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:
- How can cookies be set using the
Set-Cookie
response header using CORS? - Why can’t I see my cookies in the developer tools?. This is so common it gets a separate question in the FAQ.
- Can CORS cookies be accessed from JavaScript using
document.cookie
? - 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.
Setting a cookie with Set-Cookie
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 servers 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?.
Accessing a cookie with document.cookie
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.
Including a cookie on a CORS request
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 aWWW-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:
- Redirecting from
http
tohttps
. - Redirecting to add or remove a trailing URL slash. e.g. A server may redirect
http://example.com/api/users
tohttp://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 theOrigin
header will benull
. - Likewise, a page created using the
data:
scheme will have anull
origin. e.g.<iframe src="data:text/html,...">
. Here the...
would be the URI-encoded contents of a web page to show in theiframe
. The web page inside theiframe
would have anull
origin. - An
iframe
using sandboxing, such as<iframe src="..." sandbox="allow-scripts">
. Within theiframe
the value oflocation.origin
may be populated based on thesrc
URL but any CORS requests will have anull
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 haveOrigin: 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:
- Authentication and authorization checks to ensure the user sending the request is allowed to make the request.
- 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:
- Stealing data. e.g. It might send a request to your webmail and grab a copy of your emails.
- 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:
- Cookies.
- The
Authorization
header as part of HTTP authentication. This shouldn’t be confused with using theAuthorization
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. - 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:
- A
*
value exposes the content to any other webpage that wants it. This includes potentially malicious pages. - If the content is always the same, no matter who requests it, then exposing it to everyone isn’t necessarily a problem.
- 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 theOrigin
request header. It will still be included forPOST
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.