SameSite cookies in practice

You may or may not have heard about this new cookie attribute being slowly phased in across the web. The idea behind the SameSite attribute, is to allow for easy mitigation of CSRF by simply informing browsers when cookies shouldn’t be allowed to be sent by a third party (website).

CSRF Technical Overview

The way things work now, any normal cookie (a cookie set without the SameSite attribute) will be stored in the browser, to be presented to the site to which it is scoped upon all requests to that site. This means that if you have a cookie for “website.com” and you type into your web browser’s address bar to visit website.com; your browser will navigate to that site presenting the scoped cookie.

Of course this also means that you can find yourself vulnerable in some contexts to what is called a “CSRF” (Cross-Site Request Forgery) attack. Your cookies are what authorize you to perform actions on a site with respect to your linked account. When you log into your Facebook, or your Netflix; those sites will set a session cookie for you. This session cookie contains a unique identifier assumed to be so difficult to guess, that only you may know it. If someone else was able to guess the session identifier, they would then be able to send that secret value to the site and pose as you. But that’s a different issue…

The concern here, is that an attacker might be able to cause you to navigate to website.com in a tricky way. Like how a scam artist might try to trick you into sending him thousands of dollars for a “worthy cause.” CSRF happens when an attacker attempts to trick your browser into performing actions which can compromise your accounts on his behalf.

Remember, your browser is authorized to perform actions on the site by means of the secret value held inside the session cookie. Your browser will send that session cookie in all requests to domains for which it is scoped (ex. website.com). Therefore, if your browser was to try and do something malicious, like transfer money out of your bank account… it would be authorized to do so on your behalf by means of the cookie value.

Of course, your browser isn’t evil (presumably) and it’s not just going to try and steal your banking information. Instead, what often happens is that an attacker will set a trap for your browser. You see, not only does your browser handle requests to sites when those requests come from your typing into the address bar. Your browser is also designed to be able to handle requests which are sent via various types of code which run on websites.

For example, your browser must be able to handle HTML form elements; and to make the requests specified by them. An HTML form element looks something like this:

<form method="POST" action="/loginHandler.php" >
    <input type="text" name="username" placeholder="Enter your username.." />
     <br />
    <input type="password" name="password" placeholder="Password.." />
     <br />
<button type='submit' >Login</button>

The above might be the HTML form code behind a very simple login page. In this case, when the “Login” button is pressed your browser must know to use the “POST” method to visit the page “/loginHandler.php” (as specified by the form attribute “action”). Your browser will send the data you type into the username and password fields to “/loginHandler.php” to let the login handler decide if you should be allowed to login or not.

But what other types of forms are there? Certainly, forms are used for many things other than just logging in. We could easily conceive of a form which submits to a handler for transferring money between bank accounts. That form might look something like this:

<form method="POST" action="/transferMoney.php" >
    <input type="text" name="fromAccount" placeholder="From which account?" />
     <br />
    <input type="text" name="toAccount" placeholder="To which account?" />
     <br />
    <input type="text" name="amount" placeholder="Amount to transfer" />
     <br />
<button type="submit" >Perform Transfer</button>

In this case, when the user presses the “Perform Transfer” button, their browser submits to the handler located at “/transferMoney.php” with the values they entered into the “fromAccount”, “toAccount”, and “amount” fields.

This is all well and good… but it can go bad fast.

Remember, when the browser performs the actions specified by these forms, it is authorized to do so on the site in question by the cookies it submits along with the request. Here’s how an attacker can use that to trick your browser into doing something evil for them.

An attacker might build a form on a website they control that looks something like this:

<form method="POST" action="https://bank.com/transferMoney.php" >
    <input type="hidden" name="fromAccount" value="00000003" />
    <input type="hidden" name="toAccount" value="13371337" />
    <input type="hidden" name="amount" value="$1,000,000" />
<button type="submit" >View Dog Pictures</button>

You can see how this form is similar to the bank transfer form we wrote before… but with some very important differences. For starters, we can see here now how the attacker has modified the action to make a request across domains/sites. Remember this evil form is actually located on the attacker’s website, so now the action specifies that the browser should make a request to “bank.com” for the “transferMoney.php” handler.

Additionally, you can see that the input types have all been changed from “text” to “hidden.” This means that they will not show up as input boxes on the page. Rather, they will just be invisibly loaded into the code behind.

Finally, you can see that the attacker has set default values for all of these fields. The fromAccount is set to “00000003”. The toAccount is set to “13371337” (presumably the attacker’s bank account number). Also, the amount is set to “$1,000,000”. That’s a lot of money.

As you can see on the bottom of the form, the button which submits it no longer reads about transferring money. Instead it says “View Dog Pictures”. Presumably, the innocent victim which clicks this button will think that they are going to be viewing dog photos… when in reality, the form will cause their browser to attempt to transfer a million dollars to the attacker.

The problem here is that by default there is nothing stopping an attacker from building a website like this, which contains code he controls that will cause any victim browsers visiting it to submit requests to perform actions. If any victim browser which visits the attacker’s site has a currently valid session for that site, and if the form the attacker builds contains the correct values for the form handler on the targeted site, then it’s very likely this attack will actually work… the victim browser will send the form request, including the session cookie authorizing such an action… transferring large amounts of money to the attacker.

Fixing CSRF

The current standard technique to stop such an attack is to have a special “CSRF” cookie. This cookie (just like the session cookie) contains a random value that no attacker can guess. The website which set the cookie (bank.com in this case) can view the cookie’s contents when the browser makes a request. Remember, the browser will only send the cookie to the site which set it (to where it is “scoped” more accurately). Only the site which set the cookie can view the cookie value. The attacker doesn’t get to view the values for other cookies. The CSRF attack only works because the browser submits the cookie automatically as part of the request the attacker solicits via the malicious form.

Since the site which set the cookie can view the cookie; we can confirm that any important request (like a request to transfer money) actually came from a form on the real website (bank.com). Here’s how we do that. Take that form from earlier which performs bank transfers. The legitimate website can echo the contents of the CSRF cookie to the form as a hidden field. The new form might look something like this in HTML:

<form method="POST" action="/transferMoney.php" >
    <input type="hidden" name="CSRFToken" value="2e29461cef21d450036c6ed6060a95ff55285d91" />
    <input type="text" name="fromAccount" placeholder="From which account?" />
     <br />
    <input type="text" name="toAccount" placeholder="To which account?" />
     <br />
    <input type="text" name="amount" placeholder="Amount to transfer" />
     <br />
<button type="submit" >Perform Transfer</button>

Here, the website has (through server side code) echo’ed the contents of the CSRF cookie into the form as a hidden field called “CSRFToken”. Since only the legitimate website can view the contents of the cookie, only the legitimate website (bank.com) can ever create a hidden field with the right value. The attacker can’t do this…

So, when the request from this form arrives at the form handler “/transferMoney.php” that page can check that there is a field called “CSRFToken” at all. Then, once it finds that field, it can confirm that the value sent for the CSRFToken is the same as the one in the cookie (since the page “/transferMoney.php” is part of bank.com it can also see the value in the cookie). If the CSRFToken value matches what’s in the CSRF cookie then the money transfer can continue, since we know it could have only been generated from actions on bank.com.

This solution absolutely works, as long as the CSRFToken is strong and random (infeasible to guess); and as long as it isn’t leaked through some means such that an attacker can learn the secret value; and as long as all the form handlers on the site (ex. /transferMoney.php) all require a valid CSRFToken field matching the CSRF cookie… this absolutely stops CSRF attacks.

The problem with this method is that it’s mildly cumbersome; both to explain and to implement. There are a number of moving parts here, and as such, many sites even today still don’t implement such protections.

This is where the SameSite attribute comes in!

SameSite

The idea is to have a simple attribute for cookies, which tells the web browser whether or not it should submit the cookie in a request from a different website. Going back to the beginning of this article, we can see that the real issue here is that the browser is gullible. The browser doesn’t have insight into the effects of what it does like a human might. The browser simply performs requests when instructed to; sending the cookies it has to access domains it can.

With the SameSite attribute this will change. SameSite has two modes that it can operate in. Cookies set with the SameSite attribute can either be set as SameSite=Strict or SameSite=Lax. The difference is that when SameSite is set to Strict, the browser will not send the cookie with any cross domain requests at all, ever, period. With Lax, the browser will send the cookie with a very limited number of cross domain requests.

In Lax mode, the browser will still send the cookie when a cross-domain “top level” GET request is triggered. This is necessary because Strict mode breaks some functionality across the web. For example, if your session value for a given site is set with SameSite=Strict; when you click a link to go to that site from a different site, you’ll arrive without a session.

This would mean (for example) if Facebook implemented SameSite=Strict session values. Every time a friend sent you a link to a Facebook post (for example) in a fbook group you’re both in; clicking the link would take you to the login screen… every… single… time. Obviously that’s an issue.

This is where Lax mode comes in. Lax mode tells your browser to send the session cookie for “harmless” requests like link clicks. This way, the browser would know to not send your cookies if a malicious site instructed it to transfer money via a POST request (as in our previous example); but it would still let you click on links without having to log in every time.

(It appears that Lax mode will send the cookie with link clicks, pre-render GET requests, and via forms with method=”GET”.)

But there’s still a real issue here. If a cookie is set with SameSite=Lax, how do we know that there isn’t any vulnerable form handler on a targeted site (ex. bank.com) that will respond to GET requests? Forms should be designed in almost all cases to respond only to HTTP POST requests. But many websites have forms which are willing to accept parameters set via either GET or POST. In the case of SameSite=Strict, these form handlers would be protected. But if SameSite is set to Lax, an attacker may still be able to create a form like the one we detailed earlier, changing only a single detail:

<form method="GET" action="https://bank.com/transferMoney.php" >
    <input type="hidden" name="fromAccount" value="00000003" />
    <input type="hidden" name="toAccount" value="13371337" />
    <input type="hidden" name="amount" value="$1,000,000" />
<button type="submit" >View Dog Pictures</button>

Where before, this malicious form read: method=”POST” it now reads: method=”GET”. If the transfer handler at “https://bank.com/transferMoney.php” is willing to accept GET requests as well as POST requests, setting cookies with SameSite=Lax will not prevent CSRF attacks.

A Solution

Obviously, if a website is willing to break links directed to it, they can simply set SameSite=Strict for all cookies, and be safe. Alternatively, it’s still reasonable to use the CSRFToken approach to stopping these types of attacks, if you have the engineering skills to ensure it’s implemented correctly.

The vast majority of sites are likely to want to have the best of both worlds of course. SameSite=Lax is liable to become the go to solution to CSRF in most cases due to its support for link clicking (a very important feature of the internet). It will then be necessary for websites utilizing this protection to do essentially one of two things:

  1. Audit the entire site to ensure that no malicious action can be performed via a GET request.
  2. Implement cookie classes

Sites may simply implement two cookies for access to resources as part of a valid session. One cookie, a “read” cookie may allow the owner of the cookie to access and read resources on the site which belong to them (or to which they are otherwise permitted). In the case of something like a link click, this cookie may be set SameSite=Lax or without SameSite such that when the browser presents it, the website knows to reply to the browser by sending requested resources.

Websites then may also implement a session “write” cookie for performing actions/updates/changes to resources under the control of a given account. This cookie should be set with SameSite=Strict, such that it may never be part of a link click (if clicking a link performs an action, it is CSRF). In this way, websites may very quickly and easily ensure that all form/logic handlers are protected against CSRF by simply checking for the presence of a valid write cookie before any action will be performed other than reading allowed resources.

If websites choose pathway #1, they may have to perform complex audits of their architecture to ensure that no GET request may though some complex pathway cause an action to be performed. With pathway #2 (read/write cookies) the site has a quick way to check for valid authorization at the point the action is performed; irrespective of the ingress to that logical nexus. Forget architectural audits, and simply ensure all bits of code which perform actions check for a write cookie.

In Conclusion

SameSite cookies may help us easily create a world without CSRF. No longer will there be any excuse for not implementing protections against CSRF just because it’s deemed cumbersome. SameSite, may be set as a quick switch to protect an entire site.

Some of the restrictions created by SameSite=Strict are however very likely to leave most sites utilizing SameSite=Lax. In this case, there are rare and insidious circumstances in which CSRF may still be possible against a targeted website.

As opposed to performing indepth analysis of the architecture and logical flows of the application to avoid such insidious bugs; it will be far easier for the average service engineering team to simply implement two classes of cookie, read cookies with SameSite=Lax (or unset) and write cookies with SameSite=Strict.

Appendix A – SameSite Support

At this time, SameSite is supported in the majority of modern (updated) browsers. You won’t find support for it in some of the more legacy browsers (like old IE), or some mobile browsers. For more information on what the exact current support is, please check out this link: https://caniuse.com/#feat=same-site-cookie-attribute

Appendix B – Example Code

I had a few requests to show what the code to use a SameSite cookie would look like in practice. Obviously your deployment will certainly be different in context than what I’m about to show, but the principles here should hold.

SameSame may be implemented in a variety of languages both client and server-side. When implemented client side, you’ll be using some Javascript (or a derivative) to set the cookies. The client’s browser will then interpret that code and deploy the cookie of your choosing. For example:

Javascript

<script>
document.cookie = "bestDinosaur=Velociraptor;SameSite=Strict;path=/";
</script>

The above would cause the client’s browser to create a cookie bestDinosaur=Velociraptor with SameSite set to Strict. This cookie then could not be utilized in any sort of cross domain attack on the site that set it.

Setting cookies server side is likely a more common approach. The client’s browser anticipates to see a “Set-Cookie” header transmitted back to it in responses to requests it sends to the server. This takes place within the HTTP protocol itself. There is a plethora of languages which can be used by server-side code to communicate via HTTP to a client and thereby to set such cookies. Below are a few examples of the syntax.

PHP

PHP enjoys largely setting cookies for you as you assign sessions. However it is possible to set cookies manually should you so choose, using the setcookie function. As of PHP version 7.3 it is possible to use this function specifying samesite like so:

setcookie('name', 'value', ['samesite' => 'Strict']);

You can also set session cookie values to globally to utilize SameSite strict/lax by editing your php.ini to include the following directive:

session.cookie_samesite=Strict

Flask

You can set cookies in flask to utilize SameSite globally by editing the app config like so:

app.config.update(
    SESSION_COOKIE_SAMESITE='Lax',
)

If you are setting cookies manually, you can add SameSite to them as follows:

response.set_cookie('name', 'value', samesite='Lax')

Apache

It is possible to edit cookies inside of your web server as well. This is especially useful when you’re dealing with a language or framework that makes it hard for your to properly or easily set SameSite. For Apache, you should be able to add the following to your VHOST configuration to enable SameSite support:

<ifmodule mod_headers.c>
Header always edit Set-Cookie (.*) "$1; SameSite=strict"
</ifmodule> 

Nginx

Similarly you may use the following trick in Nginx to modify cookies. Simply edit the cookie path, appending the necessary attributes (add this to your host configuration):

proxy_cookie_path / "/; secure; HttpOnly; SameSite=lax";

About the author

Professional hacker & security engineer. Currently at Google, opinions all my own. On Twitter as @zaeyx. Skydiver, snowboarder, writer, weightlifter, runner, energetic to the point of being a bit crazy.

Comments

  1. Very detailed and informative article. Many things got clear to me after reading thru the case-study explained.

    Thanks !!

Leave a Reply

Your email address will not be published. Required fields are marked *