On the Same-Origin-Policy (SOP), CORS, Cookies and XSRF-attacks

January 2021

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 page b.com to page 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:

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.

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?

Become a better web developer by staying up to date