Is Access-Control-Allow-Origin: * insecure?

Disabling a security feature is usually a bad thing. In this case, it's fine

6 mins
An Aha! moment, delivered to your inbox every week. Check out the JS Tips & Tricks Newsletter!

CORS headers

CORS headers come into play when a client makes a cross-origin request. In that case, the server must indicate that it allows the cross-origin operation otherwise the browser will reject the request. The two important points are that the target server must allow the operation and the client’s browser enforces it.

This is a security feature as it protects the user by not letting random websites fetch data from sites he is logged in.

site.examplesite.example/apiother.example/apiNon-CORSCORS

Without an Access-Control-Allow-Origin header in the response, the browser throws an exception:

Access to XMLHttpRequest at 'https://other.example' from origin 'https://site.example'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on
the requested resource.

site.exampleother.example/apifetchNo Access-Control-Allow-Origin headerthrow exception

The Access-Control-Allow-Origin specifies the allowed origin that can make cross-origin requests. The special value * means “all origins” which essentially disables this security feature. This is usually what API developers do when faced with this error. In this case, every website can send requests to the target and read the response. And by reading, it can also forward it.

site.exampleother.example/apievil.examplehackerfetchAccess-Control-Allow-Origin: *handle responsefetchAccess-Control-Allow-Origin: *forward response

This sounds insecure, but let’s see the implications!

What * allows?

Let’s say you are a user visiting a malicious third-party website at evil.example. The site has some javascript that runs a fetch to other.example/api on your behalf. Let’s also follow the example above and assume that there is a site.example which is intended to communicate with other.example/api. Since that is a cross-origin request, other.example/api sends back an Access-Control-Allow-Origin header.

With Access-Control-Allow-Origin: *, evil.example is allowed to read responses from other.example/api. It seems like the malicious site can steal information.

But in reality, it’s usually not a problem. Without Access-Control-Allow-Credentials, another CORS header, no user identifiers, such as cookies, are sent with the request. It is like sending the request without logging in. That means a hacker has no access to information he could not gather just by sending the request himself.

other.example/apievil.examplehackerfetch without credentialsAccess-Control-Allow-Origin: *forward responsefetchresponseGets back thesame response

Intranet pages

But there is a scenario where it can be a problem. If only you have access to an endpoint then using the wildcard opens a vulnerability.

Local networkother.example/apiyouevil.examplehackerNo access

Still can’t siphon off authenticated responses but it nonetheless opens a way to detect what kind of services are running in the local network.

If your API is not public, do not use *.

With Access-Control-Allow-Credentials

To make an authenticated request (i.e. with cookies), set credentials: "include" in the request:

fetch(<url>, {credentials: "include"})

In this case, the browser checks if the response contains Access-Control-Allow-Credentials: true. But in this case * is not allowed for the Access-Control-Allow-Origin.

https://site.examplehttps://other.example/apifetchAccess-Control-Allow-Origin: *handle responsefetch{credentials: "include"}Access-Control-Allow-Origin: *throw exception

I find it a very good compromise between security and convenience. For simple cases where disabling CORS checking means (almost) no harm, the standard allows the wildcard. But when credentials are sent it requires a stricter check.

But the Access-Control-Allow-Origin can define only one origin. If you need to specify more than one (for example, your website is accessible under multiple domains) then you need more effort to make it work.

When you need credentials, which is most of the time, you need to check the origin header against a whitelist and reflect that back to the user:

const allowed_origins = [
	"https://site.example",
	"https://www.site.example",
];
const origin = req.headers.Origin;

if (allowed_origins.includes(origin)) {
	res.setHeader("Access-Control-Allow-Origin", origin);
	res.setHeader("Access-Control-Allow-Credentials", "true");
	res.setHeader("Vary", "Origin");
}

evil.examplesite.examplewww.site.exampleother.example/apifetchAccess-Control-Allow-Origin:site.example.comOKfetchAccess-Control-Allow-Origin:www.site.example.comOKfetchNo Access-Control-Allow-Origin headerException

One important thing is to also set the Vary: Origin header to let proxies know the response might be different for different Origin headers.

And also don’t forget that an origin contains the scheme and the port also. http://site.example is different from https://site.example and that is different from https://site.example:8443.

Conclusion

This is one of the rare cases where disabling a security feature does not harm security most of the time. The CORS specification is complicated with many specifics but using the * wildcard won’t harm. And when you need credentials also, you’ll see the error.

Just remember to not use it for non-public APIs.

12 November 2019