With Google’s recent announcement that all cookies without a SameSite flag will be treated as having SameSite=Lax set by default in Chrome version 80, surely Cross-Site Request Forgery will be dead? Well, not quite…
In this post I’m going to demonstrate a scenario in which the SameSite default won’t actually stop a CSRF attack from working. Spoiler alert: it takes a little bit of luck, some persistence, and a touch of magic 😉
What is the SameSite Cookie, Anyhow?
I’m glad you asked and if you didn’t, well too bad, I’m explaining it anyhow.
SameSite is a reasonably robust defense against some classes of cross-site request forgery (CSRF) attacks, but developers currently need to opt-into its protections by specifying a SameSite attribute. In other words, developers are vulnerable to CSRF attacks by default.
https://www.chromestatus.com/feature/5088147346030592
So, the SameSite cookie stops certain types of CSRF attacks. When it’s value is set to Strict this will stop the browser from sending cookies marked with the flag on any cross-origin request. Meaning that as long as web app properly checks for the presence of any auth cookies and validates them, this would thwart CSRF attacks against that web application (presuming it does these checks on every action).
Alright, cool, so problem solved right? Well, not entirely. There are two other possible values for the SameSite cookie; None, and Lax. None just means that the cookie will be treated in the same way that we are all used to, tossed along to the site from cross-domain with reckless abandonment. Lax means that in one very specific case the rule is reLaxed and a cookie will be sent cross-origin to the target domain. That case is when a top level normal link (an <a>) is followed cross-domain. So, this means that if a cookie has the SameSite=Lax flag and you click a link to the site in which said cookie belongs, it will be happily sent along with the request to the site which the link points to. This is for those times when you want users to be able to keep their sessions when they navigate to your site from cross-domain link. You know like clicking a link to a GitHub repository or whatever. It would be annoying if you had to always log back in when you did that, right?
So, what’s big deal you ask? It’s just a GET request, you say. Surely nobody mutates state on a GET request.
Yeah, riiiiiiiiiiiight.
– Me 2020
Before I go any further I should explain what it means to mutate state via a GET request and some reasons why you shouldn’t do it.
By “mutating state”, I mean causing some action on the server side that modifies some data that is meaningful to the user session or the business logic of the application. GET is defined as being a “safe method” and an “idempotent method” in rfc2616. What this means is that a GET should be treated as only a read action and that subsequent requests with the same parameters should return the same results. However, even more importantly are the security implications of mutating application state on a GET request. Because state changing requests often require some form of sensitive information that only the user should know or have, such as an API key, making the request as a GET request would mean that the API key would be more likely to be logged in several places, such as proxy logs, etc. Also the API key in this example would be leaked in the referrer header on any cross-origin redirects. Another security concern is how the browser treats GET requests. Since they are considered a simple request they are not subject to a browser preflight request. And as you’ll see in the rest of this post it can be one more feather in the cap of an attacker when trying to pull off CSRF attacks.
Suppose that you encountered a web application in which sensitive requests were all made with sensible HTTP Verbs such as our friend POST. Now suppose that either the developers had the foresight to set the SameSite flag on the auth cookie for this app (or you are using Chrome version 80 which will treat all cookies with the Lax treatment by default). What would happen if in that very same app it was possible to change a sensitive request from a POST to a GET with the POST body parameters set as URL query parameters?
Buckle Up it’s Demo Time
Below is a GIF showing normal usage of my totally secure, beautiful, responsive, progressive, money sending app, muney.dosh! It’s built using the latest in web security standards 😉
As you can see I use the app to magically log in, after which I send my dad some money (pretend you didn’t see the transfer to the loan shark 😬).
In the next image you can see that the request to login set a cookie called magictoken with the SameSite=Lax flag on it. This will stop the magictoken cookie, which the app uses for authentication and authorization, from being sent along with any cross origin requests, thus preventing CSRF attacks on the request.
Next up you can see where I “accidentally” browse to an evil site that tries to execute a CSRF attack against muney.dosh to transfer money to a hacker’s account. Silly hacker you’ve got to try harder than that! The attack fails because it is being sent cross-origin via an HTML form using the POST method, in which case the browser will not send the magictoken cookie along. That’s why the request results in a 403 Forbidden response. If you look closely at the GIF below you can see the request in BurpSuite and notice that the cookie in fact was not sent along. Cool, it works 😀
But not so fast! I may have made one mistake when building my website that a more observant hacker might just pick up on…. Below you can see me using my web app to view my past transactions (it also looks like I’ve been hacked once before 🤔). When I “accidentally” visit yet another malicious web site that launches a CSRF attack against my web app, you can see that the CSRF attack gets through, and the more clever superhacker3 send $133713371337 dollars from my account to theirs (jokes on them, I don’t even have that much money).
So How Did SuperHacker3 Hack Me?
Well, remember what I said wayyyy earlier about SameSite=Lax? Don’t bother scrolling back up, I’ve got you covered.
Lax means that in one very specific case the rule is reLaxed and a cookie will be sent cross-origin to the target domain. That case is when a top level normal link (an <a>) is followed cross-domain.
Cory Sabol – circa earlier in this very same post
Let’s take a look at SuperHacker3’s evil CSRF payload that stole all of my money.
<h1>Totally Not an Evil CSRF Attack Against muney.dosh</h1>
<strong><p>:)</p></strong>
<a id='evilAnchor' href='http://www.muney.dosh:8000/api/senddosh?recipient=superhacker3@hacker.org&amount=133713371337'></a>
<script>
setTimeout(() => {
document.getElementById('evilAnchor').click();
}, 1000);
</script>
Well now, that doesn’t look very sophisticated… What gives? I’ll tell you what, it turns out that when developing my web application I violated an HTTP best practice and then thought that I was safe and sound thanks to the SameSite cookie flag.
server.post('/api/senddosh', (req, res, next) => {
let body = req.body;
if (req.header('Content-Type').includes('application/x-www-urlencoded')) {
let b = body.toString();
let p = {};
b.split('&').map((item) => {let v = item.split('='); p[v[0]] = v[1]});
body = p;
}
sendDosh(body.recipient, body.amount);
res.header('Content-Type', 'application/json');
res.send(200, {
'status': 'money_sent',
});
});
server.get('/api/senddosh', (req, res, next) => {
let query = req.query;
sendDosh(query.recipient, query.amount);
res.header('Content-Type', 'application/json');
res.send(200, {
'status': 'money_sent',
});
});
The above snippet of code from my web app’s server side application reveals all. The endpoint to send money to users works as a POST like you would expect but it also will work as a GET request with the parameters set as URL query parameters. Why did I do this you might ask? I don’t know, why does anybody do this? Mistakes are easy to make. This opened my site up to a not so stealthy CSRF attack that works in spite of the SameSite cookie flag (I did try to figure out a way to make the attack more stealthy, but couldn’t in the end) .
The Moral of The Story
The lesson here is not that the SameSite cookie flag is bad. In fact I think that it’s a strong defense against many classes of CSRF attacks. It’s reasonably easy to implement (although I can imagine some complications for some bigger legacy apps, yikes). Additionally SameSite is going to be the default behavior for all cookies soon, unless overridden by the developer, and we will see developers loosening this up just like we see with CORS and other such security mechanisms. Also, don’t mutate state on a GET request!
P.S. you can find the horrible source code for the demo app here.