two hands wearing fingerless gloves on a laptop keyboard

Cross-Site-Request-Forgery mitigation techniques

Cross-site-request-forgery or CSRF attacks have been around for a long time, and so have techniques for mitigating them. Over the years, new options for preventing CSRF attacks have been developed. This is a showcase of the most common ones.

How a CSRF attack happens

Storing user authentication information in a cookie is a good idea. When you ensure it’s HTTP-only, JavaScript code can’t read it, making hijacking the cookie harder for attackers. But browsers send these cookies along with any request sent to the application that put the cookie originally. This is where the problem arises: What if a request to the application is triggered not by the user browsing the site but by a malicious third-party site using a hidden iframe for example? The application can’t know out of the box if the request was intentional and triggered by its front end or some completely different site.

The hidden POST form the malicious site returns to the browser might look like this:

<form action="https://trusted.com/1click-buy?productId=666" method="POST">
    <input id="qty" name="qty" type="number" value="100">
</form>
 
<script>
    document.forms[0].submit();
</script>

The attack described works because of three facts:

  • It’s possible to submit the form without any user input
  • The browser sends the session cookie with a request not originating from a page served by trusted.com
  • The server at trusted.com can’t tell where the request came from

Mitigation via synchronizer tokens – the oldest defence

To prevent these automatic form submissions from happening, we’d need to disable javascript in the user’s browser which we cannot do. Altering the behaviour of how the browser handles sending our cookies will be discussed later, this was not possible a few years ago. So the only thing we can do now is to ensure the form was posted from a page served by our webserver. We can achieve this by using so-called CSRF tokens.

A CSRF synchronizer token is a random value generated on our server for every form we return to the user’s browser. We include this value in the form as a hidden field and remember it for some time (in the DB, Redis, Memory, etc.). We may also associate the token with the logged-in user for improved security.

When a form POST reaches our server, we read the hidden field’s value and compare it to the tokens we saved earlier. If we find a token that matches, we know the form that was submitted came from a page we served to the user. If no token is found, the request came from somewhere else and was rejected.

Laravel has good documentation about CSRF tokens: https://laravel.com/docs/master/csrf

Mitigation via cookie flags – the modern approach

Using tokens as a defence against CSRF is perfectly fine and reasonably secure. The only downside is: that it requires development effort by website operators which leads to many not implementing it. Then the SameSite cookie flag came to browsers. This allowed websites to specify for which cross-site requests the cookie should be included. Three settings exist:

FlagBehaviour
SameSite=None; SecureInclude in every (HTTPS) cross-site request, as always
SameSite=LaxOnly include in GET/HEAD cross-site requests
SameSite=StrictNever include in cross-site requests

The standard behaviour was SameSite=None for a long time but eventually, browser vendors decided that CSRF should not be possible by default and altered the default to SameSite=Lax sometime in 2020. Also, the secure flag became mandatory for SameSite=None practically disallowing cross-site requests over HTTP entirely (except for localhost connections). SameSite=Lax is sufficient to prevent CSRF attacks as long as your application respects the fact that GET and HEAD requests should be idempotent and not perform any writes on data.

Two URLs are considered to be same-site when:

  • the scheme matches (HTTP/HTTPS) and
  • the domain matches (oberauer.net)

This is not to be confused with same-origin, what two URLs are only when:

  • the scheme matches,
  • the domain matches,
  • the subdomain matches and
  • the port matches

For example: when the current page is https://oberauer.net

  • http://oberauer.net is neither same-site nor same-origin,
  • https://oberauer.xyz is neither same-site nor same-origin,
  • https://www.oberauer.net is same-site but not same-origin,
  • https://oberauer.net:8443 is same-site but not same-origin,
  • https://oberauer.net:443 is same-site and same-origin
  • https://oberauer.net is the identical URL and therefore same-site and same-origin

TL;DR – What to do

Make sure your authentication cookies are explicitly flagged with SameSite=Lax in case someone uses a severely outdated browser.

Foto by Towfiqu barbhuiya on Unsplash