Skip to content

In one of the engineering teams at LeanIX we recently came across a task where a CSRF vulnerability mitigation, detected by an automated vulnerability scanner, had to be configured for one part of the app. Since I had to brush up my outdated knowledge on the topic I soon realised there is a plethora of articles on the subject out there. While each blog post helped a little, I couldn't find a comprehensive and up-to-date article where all the "moving parts" are explained in the context of CSRF. I therefore decided to put together a list of questions and answers I wish I had before diving into the implementation.

What is CSRF? #

Due to the wide array of information on the topic, it doesn't make much sense to create one more version of the same explanation. Fundamentally, the Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application. To get more practical explanation, I've found the following video perfect for understanding how CSRF attack works.

Is CSRF still a thing in 2021? #

Yes, but...

CSRF was introduced in the OWASP Top 10 2007 list, where it was listed as the fifth most critical security risk in web applications. This motivated development teams to implement a built-in CSRF mitigation solutions for various frameworks. With more and more frameworks offering secure-by-default settings and other attacks becoming more prevalent, OWASP removed the CSRF from the Top 10 list in 2017. CSRF is however still documented at OWASP.org since the actual threat is still present. By now, most of the major frameworks and content management system (CMS) provides CSRF protection out of the box. To mention just a few: Spring, .NET, Ruby on Rails, ExpressJS, Django, Laravel, JavaEE. And if you are not using a framework, there is a high chance that a battle-tested package exists for the given language.

Should I be worried about a CSRF attack? #

Yes, if:

  • you have to support old browsers. If you can block or drop the support for older browsers you'll minimise the probability for CSRF attacks. CSRF is less of a problem while browsers are becoming more secure by adding sturdier security policies.
  • your app uses the <form action="" method="POST"> tag submit approach for executing sensitive actions (e.g. money transfers, credentials change, online purchase, login).
  • your GET requests have side effects. This is an anti-pattern! If that's the case, same origin policy will not protect you and an attacker can use e.g. JavaScript fetch() API to deploy an attack using AJAX requests. Avoid this by refactoring your API handlers, otherwise your app probably has other problems besides CSRF.
  • your app enables CORS on destructive methods such as POST. If you have a valid reason to allow CORS, narrow it down to OPTIONS, HEAD, GET methods. These are not supposed to have side effects.
  • you think using CAPTCHA is safe enough. It's not. Check out this article from detectify.com.

Which browsers are problematic? #

"Can I use it" page is a go-to place to check browser support. What you are looking for in this case is the SameSite cookie support. Read further to learn about it.

How do I mitigate CSRF? #

What you can do first is to make sure your app is configured to use the SameSite cookies.

SameSite cookie provides a robust defence against CSRF attacks when supported by the browser and when used in the strict mode. However, the SameSite cookie setting is not a silver bullet. The current IETF (Internet Engineering Task Force) draft suggests that developers should still deploy the usual server-side defences, such as anti-CSRF tokens, to mitigate the risk more fully. While also ensuring that safe HTTP methods are idempotent.

What are safe methods?

Based on the section 4.2.1 of RFC 7321 methods are considered "safe" when they are read-only (e.g. HTTPS requests GET, HEAD, OPTIONS and TRACE ).

Usage #

To enable it, add the SameSite flag to your cookie:

Set-Cookie: something=xyz1; path=/; SameSite=lax

Enabling this attribute will instruct the browser to add more protection, by controlling when cookies are automatically sent with a request.

Possible options are:

  • SameSite=Strict
  • SameSite=Lax
  • SameSite=None

Strict mode #

Strict mode completely removes the chance for CSRF attack, but introduces a bit of annoying UX. The cookies will only be sent if the site (URL) for the cookie is the same as the one, user is currently visiting. MDN refers to this restriction as a first-party context.

Example: If GitHub had their SameSite cookies set to Strict mode, you wouldn't land on your GitHub account's page after clicking the github.com link here. Even if you were previously logged in. This type of navigation is also referred to as "top-level navigation".

Lax mode #

Lax mode relaxes Strict mode restrictions. It adds an exception where cookies are being sent by the browser automatically in the case of top-level navigation, but only for the safe methods. Using the same example as above, navigating to github.com using Lax mode results in the desired UX, where users land on their home page as logged in. Destructive methods such as POST remain protected against CSRF. It's now however possible to trigger CSRF via e.g. GET request, but it's your responsibility to make sure your app has a clear differentiation between read-only and destructive requests and their handlers.

None mode #

In this mode cookies are automatically attached to requests as usual. If you are using None mode, make sure you know why. You'll also need to set Secure flag to the cookie, otherwise the browser will reject it. When the Secure flag is set, the browser will not send the cookie over an insecure connection.

What is considered "same" when using SameSite cookie? #

In this context, "same site" refers to a second level domain mysite.com and top level domain mysite.com. For example if the user is on www.mysite.com and makes a request to test.mysite.com then this is considered as a "same site" request.

You might ask what about when the users are the "owners" of the sub-domain such as me.github.io and you.github.io? To make sure these are considered "cross domains" (where modern browsers aim to prevent cookies to be sent automatically) a list called public suffix list exists where all the domains, that are not under the control of the domain owners, can be registered. So for instance, since github.io is on this list, a request from you.github.io to me.github.io will result in a cross-site request.

No.

Since we can't rely on all browsers to default the SameSite flag to Lax mode (e.g. in Chrome defaults to Lax mode since version 80 - February 2020, for other check CanIUse page) we have to introduce the anti-CSRF tokens or double submit cookie technique along with the SameSite flag. These implementations should already be provided by the framework of choice or exist as an external dependency one can pull into the project.

The general rule is "Don't DIY the CSRF mitigation implementation".

Using standardised, battle-tested implementation over custom innovations especially applies in the context of security. Before trying to build your own CSRF protection, it is strongly recommended double-checking if the framework you are using has the protection already built in! If that's not the case, OWASP provides a set of CSRF mitigation techniques and use-cases here.

Few things to clear out first:

Same-Origin Policy (SOP) is a security mechanism that prevents scripts on site A from accessing sensitive data on site B. In the case of GET requests, it prevents JavaScript to read the response data. This also applies in the case when page B is embedded as an <iframe> on the page A.

Cross-Origin Resource Sharing (CORS) is not a CSRF prevention mechanism. CORS' function is to selectively bypass SOP. Or said differently, configuring CORS allows you to selectively decrease security.

In the context of CSRF this means that an attacker can still make a cross domain requests with default headers (aka "simple requests"). SOP will only prevent the attacker from reading the response data. In the case of simple requests, it's already enough since the damage has already been done (e.g. if request handler updates the state on the server side).

Simple requests, preflight request? #

Simple requests don't trigger CORS preflight. The opposite is referred to as a "preflighted request". A request is preflighted when any of the following circumstances are true:

  • It uses methods other than GET, HEAD or POST.
  • It uses the POST method with a Content-Type value other than text/plain, application/x-www-form-urlencoded, or multipart/form-data.
  • It sets any custom headers. For example, X-MYHEADER.

When we add headers that are not defined as "simple headers" (aka CORS-safelisted request headers: Accept, Accept-Language, Content-Language and Content-Type), the browser will issue a so-called "preflight request" (ask for server's permission) before issuing an initial request, using the HTTP method OPTION. The initial request (e.g. POST) is then only issued if the server responds to this preflight with a list of allowed origins, which should include the current page, i.e. response headers Access-Control-Allow-Origin and Access-Control-Allow-Method with the allowed method and domain.

Wrap-up #

Things to keep in mind when dealing with CSRF:

  • in 2021 CSRF attack still is and remains to be present
  • CSRF is not as critical anymore since mitigations have been implemented and available in the majority of modern frameworks
  • if you need to support older browsers, make sure you have mitigations in place (check caniuse page for the problematic ones)
  • if your app is not built using frameworks, there are a number of battle-tested packages for different languages out there
  • use appropriately configured SameSite cookies
Image of the author

Published by Jani Jež

Visit author page