There are a few things you need to understand why there is this pesky thing called CORS that you need to set up, if
you want to make a request from the frontend of page b.com
to a server at a.com
.
So what's preventing you from sending those requests is the Same-Origin-Policy (SOP). It states that if a
website b.com
tries to fire a (certain type of) request to a page a.com
, the browser shouldn't even send that request. But why is the browser blocking it, and
how are you supposed to make your cross origin API calls now?!
At first, what's interesting is that the Same-Origin-Policy is something that's only a thing for browsers. When you're firing a cURL request, there's no such thing. So you think, hm, but how does the Same-Origin-Policy protect my servers if someone can just fire requests against them with cURL anyways?
Well, the Same-Origin-Policy is not there to protect your servers, or at least not directly. It's there to protect the users of your website, or more broadly speaking the average Joe browsing the internet.
So how is average Joe being protected by the Same-Origin-Policy?
The Same-Origin-Policy mitigates an attack known as Cross-Site-Request-Forgery (XSRF, CSRF).
On XSRF Attacks
To understand how XSRF attacks work, we first have to understand one other thing: How cookies are working.
So let's say you're on a.com
and log in. Then a.com
returns a cookie which is then stored in your browser such that you don't have to enter your login details again
when you visit the page again. This cookie could say for example user=john;token=abc
.
The browser then attaches this cookie to every subsequent request that is sent to a.com
. And it does this (or at least used to do this) for all requests, coming from all
tabs of your browser, for any website you have open. So if b.com
then makes a request
to a.com
through javascript, the browser will attach the cookie! The server at a.com
then receives a request from a fully authenticated user. Such a request could
look like so:
POST a.com/do-something
cookies: [user=john,token=abc]
The server at a.com
then actually proceeds and does what the code behind do-something
is supposed to do for a fully authenticated user. This could be anything: Get
user data, send emails, transfer money. So in this scenario all an attacker has to do is the following:
- Set up a site
b.com
- Send a behind-the-scenes-request to
a.com/do-something
for everyone that visits the page - Lure some people to
b.com
. That's easier that it sounds, think about it, do you carefully evaluate every link you click? There's just too many link clicks in our everyday life, so nobody does that. - Hope some of the people that land on the page
b.com
are authenticated ata.com
- Let the browser do the rest, because the browser will then execute the javascript and send the request to
a.com
and attach the cookies automatically.
So why do we still browse the web with relative carelessness and trust? If every link is such a potential high risk
threat to all places where we're logged in? That's because most pages we trust and have accounts at (google,
facebook, our bank) and browsers themselves have implemented measures to prevent those XSRF attacks from happening,
so it's unlikely that severe damage is caused just by opening evilsite.com
. What
exactly are those measures?
Measure 1: The Same-Origin-Policy
The Same-Origin-Policy says that only a.com
may send (a certain kind of) requests to
a.com
. If the browser isn't allowed to send requests from b.com
to a.com
, then your problem is solved. The whole
workflow above isn't working anymore! So the SOP is a very effective measure to prevent XSRF attacks from happening.
However, the SOP isn't as strict, it doesn't block all requests. Only a category of requests. For example
simple GET requests aren't guarded by it, so it's a bad idea to have your GET endpoints have state altering side
effects (apart from other reasons). This exception makes a lot of sense, since otherwise you couldn't for example
use an image like this in b.com
anymore: <img src="a.com">. But there are also
some POST requests allowed. Those must adhere to some strict limitations, for example only the following content
types are allowed: application/x-www-form-urlencoded
, multipart/form-data
,
text/plain
. But if b.com
adheres to those
limitations and a.com
offers a corresponding endpoint (accepting a POST
with Content-Type: text/plain
for example), then an XSRF attack is still possible.
So the website a.com
can either not offer such an endpoint or, if that's not possible,
use one of the following measures. But let's first investigate the opposite case: What if the SOP is restricting you
from making a call from myapi.com
to myfrontend.com
, but
you actually own both domains and the SOP is just getting in your way of making requests?
Cross-Origin-Resource-Sharing (CORS)
The first time you might have heard of the same origin policy was maybe when you ran into a problem. Namely when you
tried the exact thing the SOP tries to prevent: Firing a request from b.com
to a.com
. We nowadays often have this scenario: the backend or at least part of it runs under a
different domain than the frontend of the page. Still, you'll have to send those requests somehow. That's where
Cross-Origin-Resource-Sharing (CORS) comes into play.
CORS lets the server inform the browser where a request is allowed to come from. While under the Same-Origin-Policy
the browser wouldn't allow any request (of a certain kind) from another origin, this allows the server to tell the
browser: "Hey look, I actually know b.com, he's a good friend of mine. I trust him". This is happening in a
so-called "preflight request". The browser doesn't send the POST (or PUT or DELETE) request immediately, it first
sends an OPTIONS request that asks "is it okay if I send you a POST request from b.com
"?
Then the server may respond with "Yeah sure, I trust that guy" (1), or "heck no, I don't know who that is!" (2) or
"uhm I don't care where a request comes from" (3).
To achieve (1), the server has to allow-list the origin b.com
in the response of the
preflight request in an Access-Control-Allow-Origin: https://b.com
header. To achieve
(2) the server actually doesn't have to do anything, that's the standard behaviour, to block cross-origin-requests.
The third option is possible by either returning a wildcard Access-Control-Allow-Origin:
*
or by checking where the request is coming from and then just putting that origin in the Access-Control-Allow-Origin
header no matter what it is. But unless you're building a public
API that should be accessible directly from the browser as well, you should only relax the SOP through CORS as
little as required, by whitelisting your known frontend-domains.
Measure 2: Same-Site Cookies
Remember how I told you that cookies are sent along on every request? Well this was not 100% accurate. While it
might have started out that way, today this isn't the case anymore. There's now an attribute for cookies that helps
servers to determine when cookies should get attached to requests. For example, if you have a backend and a frontend
that are on the same domain and you know that's the only usecase you have, then you can set SameSite=strict
.
This will then tell the browser to not attach the cookie to a request when it's coming from another origin.
So a cross origin request from b.com
to a.com
would
still get sent, but it wouldn't have the cookie attached. This (respectively SameSite=Lax
, a similar setting) is now the default for all modern browsers.
One thing to keep in mind with "SameSite=Lax"
is, that it still allows GET requests.
So, badly
designed API endpoints, that allow actions with GET requests, are still vulnerable.
Measure 3: XSRF tokens
In addition to other measures, the server sometimes issues a one-time-token to requests it expects. This means a
cookie isn't enough anymore for the server to perceive a request as fully authenticated. In addition the server also
expects that the client knows this one-time token. So if you go to b.com
and a request
to transfer your money is sent to a.com
, the server at a.com
will reject the request programmatically even though there's a cookie present because
the token is missing. The implementation of such XSRF-token workflows is most often done by frameworks, such as
Spring Security for Java or the Django framework for python.
While the previous two measures (SOP and Same-Site Cookies) mostly put the burden of implementation on browser vendors, XSRF tokens need to be implemented by web developers.
Summary: What do I need to know as a web developer?
-
XSRF used to be a very prevalent attack, because cookies from
a.com
were attached to every request toa.com
, even to those requests originating fromb.com
. -
The Same-Origin-Policy deals with this by simply not allowing (certain) requests other than to the page you're
currently on. So you can't send (certain) requests from
b.com
toa.com
. Of course sometimes you're not evil, but still want to be able to send cross site requests. That's where CORS comes into play. -
Another layer of security is the cookie attribute
SameSite
, which instructs the browser not to attach cookies to requests originating from another origin. - Another measure to prevent XSRF attacks are one time tokens (XSRF tokens).