You should still use CSRF tokens

On May 17, 2025 by Sosthène Guédon

Listen to this article
00:00 _:_ 50%

You may think that thanks to cookies being set to SameSite=Lax by default, CSRF (Cross-Site Request Forgery) is mostly a solved problem, but It's not and CSRF tokens are still good practice to implement.

The CSRF vulnerability

Let's first explain what a CSRF is and how to prevent them from being an issue.

What it is

Here's a quick example of the vulnerability:

Imagine you have a website, https://example.com. In that website, if you are logged-in as an admin, you have access to a form that allows you to give someone admin powers:

<form action="https://example.com/set-admin">
    <label for="user">User:</label>
    <input type="text" id="user" name="user">
    <button type="submit">Submit</button>
</form>

This gives you a field where you can input a username, and then click the button to submit it. Obviously, this kind of admin functionality is only accessible to users logged-in as admin.

When clicking the button, your browser navigates to the page https://example.com/set-admin?user=<value> where <value> is the value the of the user ID that you inputted into the user field. The server then checks that you are an admin, and adds the user you told it to to the list of admins of the website.

One could think that as long as the server correctly checks that you are an admin there is no risk, but that's not true.

A CSRF attack here would be simple. As an attacker controlling some other website, say https://evil.example, could embed just the exact same form on their website, changing only the labels, and try to trick you into submitting it with a username you didn't intend. Even if the form is on another website, submitting it will still work. Even worse, the attacker could make use of Javascript to automatically submit the form when the page is loaded. Now they only have to trick you into opening their site once, and they can make anyone admin on https://example.com.

How to prevent it

The usual solution to prevent such attacks is to use CSRF tokens. Essentially, the idea is to embedded in the form as an hidden element a "token", which is random and unique for each user and each session, and have the server check that the token is properly included in the form, and corresponds well to the user. Since this token is secret and per-user, the attacker cannot guess it to include it in their form, and thus cannot create a fake form on their site that would fool the server. This method is the one that should be used everywhere, ideally using facilities provided by whatever framework you are using.

There are other prevention mechanisms. All modern browser today will by default set cookies to have the SameSite=Lax attribute. This attribute means that form submissions (and JS created-requests) originating from other websites will not include cookies. This pretty much "solves" CSRF because almost all CSRF vulnerabilities require cookies to check for some authentication. But it should be noted that CSRF can still be attained. Some cookies need to be set with SameSite=None for compatibility. If you do that this protection is void. If, as in the example I gave, the form doesn't use POST submission, SameSite=Lax does not save you, because navigating to the page still sends the cookies. This is one of the reasons why you should use POST for endpoints that can change data.

We will see an example from personal experience, why SameSite=Lax or SameSite=Strict are still not enough to protect websites in many cases, and why CSRF tokens are still a good practice.

A subtle case

When I first learned about this class of vulnerability, I was a student and I worked on a student project that was hosted for all students of the school. The frontend of the project was built in client-side React and used GraphQL through Apollo for both the front-end and the backend. Apollo protects from CSRF by leveraging the CORS mechanism, which worked to prevent CSRF for the GraphQL API.

However there was one part of the backend that was using standard forms, the "adminview", available to sysadmins. I initially thought that SameSite=Lax was already protecting these forms. A couple of months ago however, I learned that the definition of SameSite is not the same concept as cross-origin requests.

Indeed, while the browser will consider a.example.com and b.example.com to be cross-origin, it will consider them to be same-site! In our case, this was a problem. In general, you own the domain and everything that goes next to it, but in our case, many students websites were subdomains of a single domain. So the project I worked on was hosted at project.example.com, and many students had access to the domains other-project.example.com. This meant that they could very well have used their websites to perform a CSRF attack on me and the other admins of the projects.

In that case, the usual protections were not enough and CSRF tokens were required to solve this issue for good!

What I gather from this

I draw multiple conclusions from this:

  • Always use CSRF tokens: You don't know what subtle configuration could be causing CSRF vulnerabilities.
  • Double protect sensitive endpoints: For particularly sensitive endpoints (such as admin panels), consider having multiple layers of security. SameSite=Lax or strict, checking the Sec-Fetch-Site header. For sensitive endpoints, it might make sense to exclude some browsers that may not include ways to perform multiple-checks.