If you deal with web APIs then CORS is a topic you will encounter multiple times. CORS is one of those areas where it’s easy to get confused. Googling makes matters worse because of the more confusing, conflicting and sometimes misleading guidance you will come across as you start reading through the threads.
Many times I have felt that I have finally understood it but that feeling lasts only for some time - till I come across a discussion that confuses me all over again. This happened (again) recently but this time I got some good, concrete guidance from a credible source to set my confusions to rest forever.
Note: This post is not meant to be a general introduction to CORS. This assumes you have read enough / lot about CORS but still don’t know how to handle it in a specific scenario (details below).
Background
You have a web API which uses bearer token for authorization. A browser based application (SPA) wants to access the API. Further the application and API domains are different (for ex. https://myapp.foo.com and https://myapi.bar.com respectively) which makes the requests from the app cross origin request necessitating CORS.
Problem Statement
-
Does the API provider need to set Access-Control-Allow-Credentials (header) to true in the pre-flight response?
-
Is it ok (from security perspective) for the API provider to set Access-Control-Allow-Origin to * or just reflect back the Origin in the pre-flight request? Note: If the answer to the previous question is yes, then it is not possible to set Access-Control-Allow-Origin to *.
Analysis
In this section I present my answers to the above questions based on first principles reasoning. In the subsequent section I share the guidance I received from external reliable source.
-
The API provider need not and should not set the Access-Control-Allow-Credentials (header) to true in the pre-flight response. The reason is that the API does not use cookie based authorization and therefore does not need the browser to automatically send the cookies during the CORS calls. The API provider however needs to set the Access-Control-Allow-Headers to Authorization so that the CORS call is allowed to pass the bearer token in this header.
-
Since the pre-flight response doesn’t set Access-Control-Allow-Credentials to true, it is OK to set Access-Control-Allow-Origin to * or just reflect back the incoming Origin header.
Here is the reason:
-
This does not make the API any less secure because the API relies on valid bearer token for auth. Setting the Access-Control-Allow-Origin to a static value doesn’t make the API any more secure because a non-browser client can successfully call the API as long as it has the bearer token.
-
The app running the browser needs to now explicitly set the authorization header to call the API. To set the header, it needs to know the bearer token which will typically be in sessions or local storage. A legitimate app will have access to its web storage whereas an exploit will not. Any attempt by the exploit to call the API on behalf of the legitimate app via browser will fail because the pre-flight response will not allow credentials and the authorization header necessitated by it is not in possession of the attacker.
-
What if the attacker gets the token from storage? If that happens, it’s a bigger concern - beyond the scope of CORS because now the attacker can just exfiltrate the token. Once the token is exfiltrated it can be used to make API calls without browser and therefore without CORS. To believe that an attacker will ONLY use the token from the same browser and that attempt will be valiantly foiled by the whitelisted Origin in the pre-flight response is naivety.
This has been my understanding but from time to time discussions and questions take me back to the confused state. For ex. statements like: you need to set Access-Control-Allow-Credentials to true even if you are not using cookies or s_etting Access-Control-Allow-Origin to * is insecure always_.
Guidance
API Security in Action by Neil Madden gave me the validation and reinforcement that I was looking for. Unlike most of the material on internet which is either incomplete in its treatment of the subject or confusing and complicated to comprehend, the book states things clearly, completely and with conviction. Here are some sample excerpts relevant to the discussion.
“Now that your API no longer needs cookies to function, you can tighten up the CORS settings. Though you are explicitly sending credentials on each request, the browser is not having to add any of its own credentials (cookies), so you can remove the Access-Control-Allow-Credentials headers to stop the browser sending any.”
“If you wanted, you could now also set the allowed origins header to * to allow requests from any origin….”
Pg. 166
“Because the API is no longer allowing clients to send cookies on requests, you must also update the login UI to not enable credentials mode in its fetch request…..
Open login.js in your editor and remove the line that requests credentials mode for the request:
credentials: ‘include’,
”
Pg. 167
“Access-Control-Allow-Credentials indicates whether the browser should include credentials on the request. Credentials in this case browser cookies, saved HTTP Basic/Digest passwords, and TLS certificates. If set to true, then none of the other headers can use a wildcard value.”
Pg. 150
This is a very important point which clarifies that Access-Control-Allow-Credentials needs to be set to true for cases other than cookies. But that is limited to saved HTTP Basic/Digest passwords and TLS certificates. If your API doesn’t use any of those, you are good.
“Google, Apple and Mozilla are all becoming more aggressive in blocking cross-site cookies to prevent tracking and other security or privacy issues. It’s clear that the future of cookies will be restricted to HTTP requests within the same site and that alternative approaches must be used for all other cases.”
Pg. 152
If you are thinking of cookies based web API auth, drop it right away. I wish I had this book with me few years back when I had to convince people the hard way to not mix cookies and public APIs.
Additional thoughts
-
If the idea of storing bearer tokens in web storage and concerns you because of exfiltration/XSS threats, first thing to note is that this is an app level concern and not an API level concern. This is not related to CORS. One way to address the concern is employing the BFF pattern.
-
In some cases you may have a web API but want to lock down the web apps which can use the APIs to a dynamic but well known finite list. In such cases you can have a registration mechanism to associate the app origin to a unique identifier (for ex. Client id / App id / Tenant id) that is used while making API calls. With this in place, it is possible to do a dynamic lookup during pre-flight and return the exact Origin associated with the app. More on it a separate post.
Conclusion
I think I have finally attained CORS nirvana. Even if it not, I’m very happy that I found a good resource with some categorical and clear guidance on this complicated and confusing topic.
I have not read the whole book yet but the few parts of it I have read so far have proved to be super helpful. I highly recommend the book to anyone and everyone who works with web APIs.
Thank you! Neil Madden for this lovely book.