loading...
Cover image for Dealing with CORS

Dealing with CORS

stegriff profile image Stephen Griffiths Originally published at stegriff.co.uk ・4 min read

If you're a web developer, CORS is probably a recurring phantom that comes back every other project to steal a whole week. After my most recent wrestle with it, I wanted to write a playbook to the common scenarios, gotchas, and workarounds. I hope it helps you!

Cross Origin Resource Sharing (CORS) is a security measure enforced by web browsers which affects calls made from a web browser to a remote resource like an API on another web domain.

The key to understanding CORS is to understand that it is the browser which is blocking, but to allow CORS, the server has to add special headers which satisfy the browser into allowing the connection. As such, CORS blocking doesn't affect back-end solutions that call APIs.

1. Simple requests

If you are just doing an HTTP GET, HEAD, or POST (with no custom/exotic HTTP headers and a "normal" content-type), then you're making a simple request.

Simples

This means there won't be a 'preflight' OPTIONS check.

The simple content-types are:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

Watch out for application/json!

Here's a big Gotcha - if your API speaks JSON, using Content-Type: application/json (for example), then that counts as an exotic content type, and you will be subject to a preflight OPTIONS request!

Not who you were expecting

My call really is simple

Well, after calling for the endpoint you wanted, like https://coolapi.example.com/pets/, the browser checks whether the response contains an Access-Control-Allow-Origin header which lists your site by name or implies it by wildcard (that is, *).

If there's no such header, or your site isn't mentioned in said header, the browsers thinks "Ok, that API isn't supposed to be called from here (perhaps an attacker is trying to scrape user information)" and it stops the running JavaScript from seeing any detail about the response except that it failed. The F12 developer tools in the browser can tell the developer/user what happened, but the (potentially evil) script is kept in the dark.

So, to allow calls to your API from any and all websites, as long as you don't require special headers or anything else mentioned on the MDN description of a simple request, you only have to make sure that your server returns the aforementioned Access-Control-Allow-Origin: * header.

2. Complex requests

CORS warning in F12 console

Complex, or "preflighted" requests are those with extra headers or an unusual content type, and have to be checked first (by the browser) to find out whether the request is safe to send at all.

When the browser thinks a request is complex, it will call ahead to the server with the HTTP OPTIONS verb.

Your server must respond to the OPTIONS verb not only with a matching Access-Control-Allow-Origin but with other headers that explicitly allow whatever incoming stuff you expect in the request. For example, if you expect a header called X-Secret-Allergies then your server must respond to the OPTIONS call with a header like:

Access-Control-Allow-Headers: X-Secret-Allergies

To allow content types that aren't in the "simple" list above, you have to explicitly add the Content-Type header to the list in Access-Control-Allow-Headers!

A further complication - Authorization header and cookies!

If your request requires an Authorization header, or an auth cookie of some kind, you can't use a wildcard for your Allow-Origins list! Yikes! You have to specify domains. I don't know how people are working around this in the wild, except perhaps by rolling their own auth headers or doing some RESTful credentials-in-every-request.

If you're using a framework to abstract HTTP requests, like Axios, Angular $http, or (cough) JQuery AJAX, then you should look for the withCredentials option. Setting it to true means that you are sending either an Authorization header or an auth cookie. So you can't have a wildcard allow list, and you must add Authorization to your Access-Control-Allow-Headers list.

3. Why. What. Why do I have to do this. Wh-

Here is the best explanation of an attack which is stopped by CORS that I have found.

It seems to me that in essence, CORS helps to protect users from evil sites which try to load resources from good sites, hoping that the user is logged into the good site, so that their account information can be pulled across to this evil page and sent off to an evil server.

4. Testing tool

Check out https://cors-tester.glitch.me/

There may be plenty of tools out there for testing CORS, but I'd like to add mine to the pile.

This is an open source tool hosted on Glitch, which keeps your requests/responses entirely private; they go straight from your browser to whatever API you type in. If that's not good enough, you can even download the two files and run index.html on your own PC by simply opening it in a browser (no local server required)

5. End

I hope this article and the tool help you out.

Tell me about your CORS nightmares in the comments!

Originally published at stegriff.co.uk

Posted on by:

stegriff profile

Stephen Griffiths

@stegriff

Aspiring Jesus follower; successful nerd. Pro full-stack developer 8+ years, rapid prototypes for fun.

Discussion

pic
Editor guide
 

My strategy is this:

  • Use Docker-compose
  • Use an nginx container
  • Route calls to /api/ to my back-end (Python container running Django, typically)
  • Serve static assets and my built front-end from nginx

Setting up docker may take a while the first time, but once you learn it, you never have to worry about CORS, Cookie origin, etc.

 

I guess the trick here is that both your backend and frontend are at the same origin because of docker, so you don't have the problem. But when you go into production, the landscape might be different, e.g. if the backend one day is run from a different remote host.

Thanks for the insights :)

 

I generally run them on a single host, as I run smaller sites, but even if you run them on multiple hosts, as long you run nginx (or HAProxy, or something else - I only have experience with nginx), it doesn't really matter.

In your nginx config:

location /api/ {
  # https://example.com/api/
  proxy_pass http://backend.example.com;
}

location / {
  # https://example.com - everything except for /api/
  proxy_pass http://frontend.example.com;
}

Of course, this is a simple example that might break TLS, if you're proxying requests over the internet, and you probably want to pass more options (e.g. forwarding the HOST header to django), and this may add some latency, so ideally you'd want all servers running in the same data center.

I'll see if I find the time to write a more detailed post about this one of these days.

 

I don't really understand. Does this solve the problem with Webpack Dev Server or Gatsby?

 

I've never used Gatsby, and the webpack work I do is usually with vue.js, but if your webpack dev server is communicating with an API, yes.

I run similar containers (Django for back-end, vue.js for front-end) on my dev machine as in production.

The difference is I serve a built SPA from nginx in production, and run the nodejs server in dev - but then I still have an nginx container for routing.

I point my base URL to /api/ (or often to /graphql/), then it doesn't matter whether I access my dev site through localhost, or dev.example.com, or if I access staging.example.com, or just example.com in production... It Just Works™.

 

One point is if the TLS certificate doesn't match the actual server domain. That is a problem with dev.to's api. There is a mismatch and browsers will block it. I had to resort to a work around to access dev.to api. dev.to/jrsofty/cors-problem-workar...

 

This is good.Thanks

 

Thank you for reading!

 

This is really helpful. Thank you

 

I'm glad! Thanks for the feedback :)

 

I once setup a lambda in aws to make requests client side which would then request another resource. Sort of overkill but still cool to try out.

 

How do I solve CORS when using Webpack dev server?

A solution is probably with expressjs's cors to allow all when process.env.NODE_ENV === 'development'.

 

I suppose it's up to you in your Express backend to make sure that the right headers are sent back as described, and that your backend responds to OPTIONS requests if necessary. Express seems to have its own CORS guide here: expressjs.com/en/resources/middlew...

Hope this helps :)

 

Great article ! Thanks !