Just enough CORS to not get stuck
Cross-Origin Resource Sharing or CORS is one of those things that I have had to explain to a lot of junior folks. I feel like it is partly because there is a lot of unhelpful and confusing information around it. In this blog, I hope to give a high level overview of what the idea behind CORS is, why it is required and most importantly how to avoid getting CORS errors in your browser.
Image credit: Good Tech Things
For most people seeing the below error is enough to make them give up on the project or to start searching for "how to disable CORS in my browser".
Why do we need CORS #
Before we get into what CORS is, let's see why it is needed in the first place. As always, the reason why we need more complex things is because of security.
CORS is necessary when you want your JavaScript code running in the browser to be talking to a different domain. For example, you might have your UI on ui.example.com
and your backend on backend.example.com
. Now, if you want your JavaScript on the ui.example.com
page to be able to make a POST
request to your backend at backend.example.com
, then the backend server should allow for it to happen via CORS.
Why is this a problem. Can't we just allow requests to any domain?
Good question. Let me paint you a picture. Let's say you have your bank account with ABC Bank. You wake up in the morning afternoon and start your day off by checking your bank balance. I would ask why you are checking your bank balance first thing in the morning, but I digress. Let's see how your bank website might go about letting you do this.
You log onto abcbank.com
with your username and password. It loads and HTML page. Now you click "See balance" button. The website makes a request to api.abcbank.com
and get the balance. It shows your balance as 500. You instantly donate 80 to FSF(or EFF if you think FSF is too edgy) so that your balance is 420. To perform these operations(getting balance and doing a transfer), it uses cookies that you have generated when you logged in to authenticate you.
Now that you are done with your morning banking stuff, you close the tab and go check your email. You see a message about Raspberry Pi finally being available. You instantly click to open the message. Suddenly you get a notification on you phone saying you just transferred 351 to a different account.
What happened there? Looks like email about Raspberry Pi being available was a scam and the link you clicked ended up making a request to your bank to do transfer the money. You browser happily sends the auth token along with the request even though the request was made from rpiforsale.com
. This particular kind of vulnerability is known as Cross-Site Request Forgery (CSRF).
Suddenly you wake up from the dream and realize you live in a world where browsers are not that dumb. All your money is safe to spend of useless things of your choice.
Woah, I need CORS. What is it? #
I'm glad you came around. Now that we have seen why you need it, lets see what it is.
By default, your browser does not allow you to make requests to a different domain. This is known as the same-origin policy.
CORS at its core is a way for a sever to let the browser know what kind of requests it is allowed to make and what all data is allowed to be sent in the request. For example it can say, "I allow GET
requests from example.com
and POST
requests from example.com
with Content-Type
as application/json
".
Here is an image I took from MDN docs explaining CORS. Something visual to look at after staring at a wall of text.
How does it work? #
Good question. Let see how does the whole thing actually works. Every time your browser has to make any request that can cause side-effects(HTTP methods other than GET
, or POST
with certain MIME types), your browser does something called a "preflight" check. This is way for the browser to ask the server what it is allowed to send the request that it is about to.
It does this by sending an OPTIONS
request with headers like Access-Control-Request-Method
,Access-Control-Request-Headers
,Origin
,Host
,User-Agent
and your server can take a look at those and decide if it want to allow the upcoming request. Response headers like Access-Control-Allow-Credentials
can let the browser know whether or not send credentials in the request.
For those who don't know, an OPTIONS
request is just another method like GET
or POST
that the server can handle. You can process it just like working on any other request.
Let's see an example of how this would play out our in our original bank scenario. When the RPI scam website asks the browser to make a request to the bank to do the transfer, the browser will do a "preflight" check. It does this by making an OPTIONS
request to the bank's api server(api.abcbank.com). The important bit here is that the browser would send the Origin
header as rpiforsale.com
in the request. Then bank's backend, on seeing that the request does not originate from the bank's website returns a 403
status code. When the browser gets this, it fails the POST request and you money will remain safe with you 😮💨.
Non solutions #
Accept everything from everywhere #
This is useful when you are just getting started or don't care who/what is making the request. This is the same as saying "I don't care who you are, you can come in and take whatever you want". At the minimum, should only be allowing requests only from the domains that you are expecting requests from. The problem is that a lot of "tutorials" just ask you to use something like corydolphin/flask-cors which basically does the same thing. This is also what shows up at the top when you search for "python cors" which is not helpful. The StackOverflow top answers also points you in the same direction.
Using browser plugins to avoid CORS #
I have seen a couple of junior folks saying "you have to install the CORS plugin to fix that issue". They are referring to plugins like this which "disable" preflight checks. This, first of all is bad, and second of all will only work on browsers where you have to installed and is not something you should be asking other people to do.
Seating mode
as no-cors
#
Another place I've seen people get stuck is folks seeing an option to set mode
as no-cors
. This, although avoids preflight checks has a couple of issues. You cannot use credentials plus you cannot read the response body.
What to actually do #
Now that we have discussed what not to do, let's see what you should be doing.
You should treat OPTIONS
as just another method on the endpoint and properly handle it. "Properly handling it" at the bare minimum includes making sure that the origin specified there is something you are expecting a request from. You can also leverage the data in the other headers that the browser sends you, ie method, headers etc to determine if you should be accepting the upcoming request. If you don't think the request should come through, then you can return a 403 back to indicate that to the browser.