Cookie’s SameSite Attribute vs CSRF — Demystified
Ambient Authority in the browser
Whenever the browser sends off an HTTP request, it includes all the corresponding cookies for the request target. This model for using authority is called ambient
because the requestor doesn’t explicitly specify what authority should be used. Instead, the execution environment automatically adds all possibly relevant authority to every request.
After finishing this article, you will understand the nitty-gritty of this feature including the side-effects.
Background
We have a legitimate bank named my-ban
k with its website at my-bank.com:8000
. The login page for the banking service is as shown below.
Suppose alice
logs into the bank using her credentials. She will get redirected to her dashboard which shows she has $500 in her account and the bank provides the service to transfer any amount (given that you have sufficient balance) to any person you desire.
Then the server provided alice
with a cookie named sessionId
for better session management. Lets focus on the cookie attributes.
sessionId=Wgg2yTD4ZqAwC11HvlwifQ%3D%3D
represents name-value pair as set by the serverDomain=my-bank.com
instructs the browser to attach this cookie to all subsequent requests sent to its subdomains too (Remember! specifyingDomain
is less restrictive than omitting it)Path=/
means the cookie is valid all over the site regardless of any path requestedExpires
attribute defines when should the cookie be removed from the browserHttpOnly
if set will prevent cookies to be access by the JavaScriptSecure
restricts the cookie transfer to happen only over HTTPSSameSite
is the major attraction of this blog. We will talk about it the whole time.
Remember the SameSite
attribute is set to None
in this case.
SameSite: None
Back to my-bank
dashboard. Alice just transferred 100$ to bob’s account. Here’s what the POST request to the server looks like.
POST /transfer HTTP/1.1
Host: my-bank.com:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 17
Origin: http://my-bank.com:8000
Connection: keep-alive
Referer: http://my-bank.com:8000/
Cookie: sessionId=Wgg2yTD4ZqAwC11HvlwifQ%3D%3D
Upgrade-Insecure-Requests: 1
to=bob&amount=100
Now, enters the attacker
. By some means, the malicious attacker was able to send a legitimate-looking link cute-cats.com
to send Miss alice
. Let’s analyze the HTML of cute-cats.com
.
<body>
<h1>Cute Cats</h1>
<img src="cat.png" width="500px">
<iframe src="malicious.html" frameborder="0" style="display: none;">
</body>
So basically the site is using an iframe with visibility:none to embed malicious html code inside it and to show its innocence, it is displaying a picture of a cat on the page. This is the HTML code of malicious.html
.
<form method='POST', action='http://my-bank.com:8000/transfer/'>
<input type="text" name="to" value='bob'>
<input type="text" name='amount' value=100>
<input type="submit">
</form>
<script>
document.forms[0].submit();
</script>
What this is basically doing is sending a POST
request to http://my-bank.com:8000/transfer/
endpoint with the post data to=bob&amount=100
that will effectively transfer 100$ from alice
account to bob’s account.
alice
clicks on link forwarded to her. I mean, why not? Who don’t love cats?
As soon as the page loads, the malicious code inside the malicious.html
starts to show its colour. This is the POST request that will be sent by this innocent-looking page. Observe the Origin
and Referer
headers.
POST /transfer/ HTTP/1.1
Host: my-bank.com:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 17
Origin: http://cute-cats.com:8000
Connection: keep-alive
Referer: http://cute-cats.com:8000/malicious.html
Cookie: sessionId=Wgg2yTD4ZqAwC11HvlwifQ%3D%3D
Upgrade-Insecure-Requests: 1
to=bob&amount=100
And this is what alice
will see. Awww! How innocent!
And this happened behind the scenes, successfully transferring 100$ to bob’s account.
Now i don’t think these cats are really cute anymore, right?
The main reason for this incident is the Ambient Authority mechanism of the browser which inadvertently add the session cookies with all the requests to my-bank.com
disregarding where the request actually originates from. Neither had the cookie embedded, nor this attack would be successful. And all this credit goes to SameSite: None
attribute .
SameSite: Lax
Cross-site POST request using iframe
This time we have set our SameSite
attribute to Lax
(other settings are the same). Lets see what it have got for us.
When alice
goes to the cute-cats.com
page this time, the request sent is different as shown.
POST /transfer/ HTTP/1.1
Host: my-bank.com:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 17
Origin: http://cute-cats.com:8000
Connection: keep-alive
Referer: http://cute-cats.com:8000/malicious.html
Upgrade-Insecure-Requests: 1
to=bob&amount=100
See how no cookies were attached to this POST request which means server could not authenticate the user and no balance transfer process was carried out effectively protecting us from the attack.
Cross-site GET request using iframe
Lets modify the cute-cats.com
site’s HTML. We changed it to make GET request this time. And the iframe visibility is true i.e, we can see the page that will be embedded.
<body>
<h1>Cute Cats</h1>
<img src="cat.png" width="500px">
<iframe src="malicious.html" frameborder="0" width='100%'>
</body>
<form method='GET', action='http://my-bank.com:8000/'>
<input type="submit">
</form>
<script>
document.forms[0].submit();
</script>
Cookies were not sent with GET request this time too. The request below explains it all.
GET /login HTTP/1.1
Host: my-bank.com:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://cute-cats.com:8000/malicious.html
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
And this is what alice
will see. The bank’s login page embedded inside the cute-cats.com
page. Weird!! But the real thing to notice here is we have evaded the attack.
Thus we can conclude that cookies are not sent by the browsers if the cross-site request is made by the page’s sub-sources ( anything that has to be fetched after the HTML has been parsed like scripts, stylesheets, images, iframes). But what about forms ?
Cross-site POST request using forms
<body>
<h1>Cute Cats</h1>
<img src="cat.png" width="500px">
<form method='POST', action='http://my-bank.com:8000/transfer/'>
<input type="text" name="to" value='bob'>
<input type="text" name='amount' value=100>
<input type="submit">
</form>
<script>
document.forms[0].submit();
</script></body>
Cookies were not sent in this case also. Eventually, server redirected us to the homepage. Since GET will be used to fetch the homepage, cookies will be attached and we are logged in, effectively saving us from the attack.
POST /transfer HTTP/1.1
Host: my-bank.com:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 17
Origin: http://cute-cats.com:8000
Connection: keep-alive
Referer: http://cute-cats.com:8000/
Upgrade-Insecure-Requests: 1
Cross-site GET request using forms
<body>
<h1>Cute Cats</h1>
<img src="cat.png" width="500px">
<form method='GET', action='http://my-bank.com:8000/'>
<input type="submit">
</form>
<script>
document.forms[0].submit();
</script></body>
This is only the condition when cookies are attached with the request in SameSite: Lax
scenario. So we will be redirected to my-bank
dashboard logged in.
We can conclude that SameSite: Lax
prevents sending cookies to any cross-site requests that originate from sub-resources. And it also prevents cookies from being attached on any request which isn’t (supposed to be) read-only such as PUT, POST. That’s why cross-site GET requests will have the cookies attached as it is considered safe method.
SameSite: Strict
Cross-site GET request using forms
GET /? HTTP/1.1
Host: my-bank.com:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://cute-cats.com:8000/
Upgrade-Insecure-Requests: 1
Cookies were not sent in this case. The server responded with 302 Found
and redirected us to the login page. Since the browser won’t send cookies even with GET requests, we are not logged in in this case. The cool part is refreshing the page once again will log you in. Think why.
HTTP/1.1 302 Found
X-Powered-By: Express
Location: /login
Vary: Accept
Content-Type: text/html; charset=utf-8
Content-Length: 56
Date: Tue, 08 Jun 2021 11:20:52 GMT
Connection: keep-alive
Cross-site POST request using forms
Its not even allowing cookies with GET requests. So how dare you ask if it will be sent with POST request. Obviously NO.
Actually, to make it clear SameSite: Strict
completely blocks sending cookies in the cross-site requests case. Even when clicking a top-level link on a third-party domain to your site, the browser will refuse to send the cookie.
Is CSRF dead?
In old days, SameSite: None
was the default case (allowing third-party requests by default). Instead of leaving the user’s cookies exposed to potential security vulnerabilities , the Chrome 80 update takes the power back and sets all cookies to SameSite=Lax
by default. In other words, Chrome has decided to make all cookies limited to first-party context by default, and will require developers to mark a cookie as needing third-party visibility using SameSite=None
explicitly.
So is CSRF dead? Or you have any other idea.
Founder of cybersecnerds.com. Electronics Engineer by profession, Security Engineer by passion.
I am a Linux Enthusiast and highly interested in the offensive side of the CyberSec industry. You will find me reading InfoSec blogs most of the time.