The opinions expressed on this blog are purely mine

How did I waste 6 hours finding out all about Same-Site cookies?

2017-04-08

Note: This post was originally published on medium.

Let me share a rather dull story with you about the misery I have suffered last night. I hope after reading this you will know a lot more about CSRF prevention and Same-Site cookies, and you won’t be sentenced for hours of debugging.

The story begins with a project I had last year. The project’s backbone was a Hapi.js backend which implemented a PayPal payment flow trough PayPal JS SDK. The idea was to store each PaymentID* in a Cookie for each individual user session so that I can reuse the ID during the payment process and I am able to validate the requests for given resources. This project was a successful one, all milestones were completed, everything worked fine.

*PaymentID is a unique ID generated by PayPal to identify each paying sessions

Last week I started to work on a new project which also implements PayPal payment, so it was obvious to go back to that old project, grab the usable parts and start to code from there. As finished the code responsible for payment I started to test the website, and I was surprised to see that after the Paypal redirection the execution resource dropped me a nice and fat HTTP 401 error code. For those of you who knows nothing about how Paypal SDK works I wrap it up in the next section.

How would the payment flow work in my ideal world?

1
There a list of products, prices and other meta information in JSON format, that is sent (POST) to a resource (for example http://localhost:8000/pay). The JSON is formatted by the rules given by PayPal in order to be valid. The resource is requested, and the handler creates a Paypal payment object from the JSON. Inside the JSON there is the return_url defined, to which the browser redirects after finishing the payment on PayPal’s website.
2
Right after the PayPal object was created, it produces a PaymentID which is stored in a cookie. It serves multiple purposes, the most important is, that it is only allows the payment to execute, if there is a cookie in the client’s computer with the right PaymentID. In other way, when the browser redirects from PayPal’s website, one is able to enter the /execute resource only if it has the right cookie.
3
In case the user were authenticated to use the /execute resource, the payment proceeds, finishes and other database activities run. Cool.

The problem was, that I couldn’t get inside this /execute resource despite all efforts. The cookie I used in my project was the hapi-auth-cookie plugin. My first take on the problem was, that there were some radical changes in the inner functions of this plugin since last year. Then I checked it, no fundamental changes were mentioned on its github page.

My second idea was, that the url mapping of /execute resource messed up the authentication somehow. The PayPal puts some query parameters in the return url, so /execute becomes something like this:

execute?PaymentId=PAY-LDTMNEI&token=EC-5124K&PayerID=4LX3STQ

So my idea was, that the resource was registered for authentication without the parameters, so adding them messes up the process. I know it is stupid, and I knew very well that this should work, but I couldn’t find any other reason. After a few hours code/documentation/API reading, debugging, refactoring something happened. When the url was stuck in the above quoted state, I accidentally hit enter. Lo and behold, the resource allowed me to enter, the payment finished, the database was saving the logs. I went trough the whole process instantly again, and after the redirection I was stuck once more. But, hitting enter let me proceed further again. A well-placed page reload after the redirection could save the day, but I didn’t like the idea as it would have omit the problem. I turned to Google in order to find some explanation to this phenomenon. That is where I stumbled across CSRF and the world of same-site cookies.


SRF a.k.a Cross Site Request Forgery is a method, in which an attacker makes authenticated requests on a website with the help of your valid cookie. The attack is possible due to third party cookies. When you enter a website, your browser sometimes makes request to other websites as well because of Facebook’s like button or other embedded content on the originally visited site. When these requests are made, the cookies stored in your browser belonging to Facebook, etc. will be attached. This way the third party websites can track your internet activity, like Facebook recently.

Same-site cookies (née “First-Party-Only” (née “First-Party”)) allow servers to mitigate the risk of CSRF and information leakage attacks by asserting that a particular cookie should only be sent with requests initiated from the same registrable domain. — Chrome Documentation

To prevent these tracking actions and malicious attacks, browsers came up with the idea of Same-Site Cookie attribute. With that, developers have the ability to decide upon the cookie’s behaviour. The two possible values for this attribute are lax and strict.

1
Scrict: When strict is set, the cookie should not be sent with the request initiated by third-party websites.
2
Lax: When the attribute is set to lax, the cookie will be sent in addition the GET request initiated by a third-party website. However there is one restriction, the request being made should cause a top-level navigation. Meaning, the browser’s url address bar should change by the navigation action. This restriction rules out iFrame and AJAX requests.

So how all this adds up? In my recall, the problem was, that by default my cookie was not being sent to PayPal in the first place, so when the browser got redirected to my website right after the payment, Paypal couldn’t provide with the appropriate cookie. (SameSite cookie feature was implemented in Chrome and other browsers in April 2016, that is why my new project didn’t work.)

The solution

    
server.auth.strategy('session,'cookie’,{
    password: ‘SECRET',
    cookie: ‘session',
    ttl: 3 * 24 * 60 * 60 * 1000, 
    isSecure: false,
    isSameSite: ‘Lax'
})
    

The solution was adding isSameSite: ‘Lax’ the the strategy definition.

I do hope that you will find this article useful, and you won’t run into the same mistakes I did during this project.

Cheers!

If you would like to get a more detailed explanation