DEV Community

Cover image for Using a Cookie-to-Header CSRF Token in Single Page Applications
Nick Scialli (he/him)
Nick Scialli (he/him)

Posted on • Originally published at typeofnan.dev

Using a Cookie-to-Header CSRF Token in Single Page Applications

The Cross-Site Request Forgery (CSRF) attack vector is often misunderstood. Today we'll gain a better understanding of CSRF and why cookie-based CSRF tokens are a good option for Single Page Applications (SPAs).


If you enjoy this tutorial, please give it a 💓, 🦄, or 🔖 and consider:

📬 signing up for my free weekly dev newsletter
🎥 subscribing to my free YouTube dev channel


What is a CSRF attack?

A CSRF attack is when an attacker website is able to successfully submit a request to your website using a logged-in user's cookies. This attack is possible because browsers will "helpfully" include cookies with any request to your site, regardless of where that request originated from.

Let's go through the motions of what a CSRF attack might look like.

User logs in to your site and interacts with it normally

A user navigates to our website and submits their email address and password to our server. Our server validates this information and sends a cookie called sessionId to the client. The client now starts making requests to the backend, sending the sessionId cookie along as it goes.

user login flow

User navigates to an attacker's website, which makes a POST request to your backend

At some point, the user navigates to an attacker's website (let's say attacker.com.... sounds menacing, right?). The attacker knows enough about our website to know we have a /profile endpoint that accepts post requests, and that if a user posts a new_email to that endpoint, that user's account email is changed.

So while the user is on attacker.com, the website shoots off a post request to our website's /profile endpoint. The browser says "oh! I have a cookie for this website, let me helpfully attach it to this request!"

the attack

Of course, that's the last thing we really want to happen since an attacker has now posed as a logged-in user and changed that user's email address. The attacker now has control of that account—requesting a password reset on our site will send a reset link to the attacker's email address and they're in!

Does CORS Protect Me Against CSRF Attacks?

Cross-Origin Resource Sharing (CORS) does not protect you from CSRF attacks. CORS is a header-based mechanism that tells clients what origins are allowed to access resources on a server.

Let's say your frontend is located at https://www.yoursite.com and your backend is located at https://api.yoursite.com. In response to any request, you can configure your backend to basically say "the only origin that I want to access my resources is https://www.yoursite.com ."

Access-Control-Allow-Origin: https://www.yoursite.com
Enter fullscreen mode Exit fullscreen mode

And this will work! For example, if attacker.com tried to get data from a CORS-protected API endpoint on your backend, the request would fail because the browser wouldn't allow the attacker.com website to see the response to that request. But that's not what a CSRF attack is—the attacker doesn't need to see the response from the POST request; the damage has already been done when the request is made!

TL;DR: CORS protection is extremely important, but it doesn't do anything against CSRF attacks.

So What Does Protect Me from CSRF Attacks?

The defense against a CSRF attack is to use a CSRF token. This is a token generated by your server and provided to the client in some way. However, the big difference between a CSRF token and a session cookie is that the client will need to put the CSRF token in a non-cookie header (e.g., XSRF-TOKEN) whenever making a POST request to your backend. The browser will not automatically make this XSRF-TOKEN header, so an attack could no longer be successful just by posting data to the /profile endpoint.

Using Cookies for CSRF Tokens in Single Page Applications

Wait what? Cookies are the reason we're in this mess in the first place, how can we use cookies for CSRF protection?

Well it's important to remember that, when we make a POST request to our backend, the backend doesn't want the CSRF token to be in the Cookie header. It wants the CSRF token to be its own header. An attacker simply wouldn't be able to add that CSRF-specific header and the browser certainly isn't going to do it for them.

Using a Cookie-to-Header CSRF Token

So if we add a CSRF token to our diagrams above, here's what we get.

csrf flow

And if our attacked tries to do a POST request, they have no way of providing the XSRF-TOKEN header. Even though our browser will send an XSRF-TOKEN cookie back automatically, our backend simply isn't looking for it.

attack-attempt

Why I like Getting the CSRF Token in a Cookie for SPAs

There are a few different ways the backend could provide our for our SPA: in a cookie, in a custom response header, and in the response body.

The main reason I prefer the cookie method is that we don't have to do anything special for our browser to hold onto this information: when a cookie is sent by the server, our browser will automatically hold onto it until the cookie expires (or the user deletes it). That means the XSRF-TOKEN cookie will be waiting there until we need it. If, however, our server was sending us the CSRF token in a custom header or the response body, we would have to proactively handle that response information in our JavaScript code. We could shove it into our app state or set a new cookie, but we'd have to proactively do something.

As an added bonus, some HTTP request clients like axios will automatically look for an XSRF-TOKEN cookie in our browser and will turn it into a custom header whenever sending a request! That means we don't even have to do anything fancy when posting data to CSRF-protected endpoints.

Important Configuration Notes

There are some "gotchas" when going the CSRF-in-cookie route.

First and foremost, your SPA needs to be at the same domain. For example, if your backend is at api.yoursite.com and your SPA is at www.yoursite.com, you'll be in good shape by just adding an additional DOMAIN property onto the cookie. However, if your backend is at api.yoursite.com and your SPA is at www.othersite.com, then your frontend will not be able to read the XSRF-TOKEN cookie and you'll want to go a different route with your CSRF token.

Next, the only way this works is if our JavaScript code has access to the cookie. This means our server cannot set the XSRF-TOKEN to be HTTPOnly (HTTPOnly means our client/browser can send the cookie back to the server but our JS can't see it).

One config details with axios is that it specifically looks for an XSRF-TOKEN cookie and, if it finds it, it'll send the token back as an X-XSRF-TOKEN header. This is all configurable, but you'll want to make sure you configure it correctly or you'll be wondering why it's not working.

The Future: SameSite Cookies

This is all good and fine, but CSRF protection is really just a fix for some strange browser behavior (automatically attaching cookies to any request to an origin). Setting the SameSite cookie property can fix this: if a browser sees a cookie with the SameSite attribute set to either Lax or Strict, it won't send a POST request to a server unless that request originates from the same site (same protocol + domain, but subdomain can be different).

This will be great once it's univerally supported—the SameSite cookie property is relatively new. It's up to the browser to understand what a SameSite cookie even is and, if someone is using an older browser that doesn't understand what SameSite is, then that user will be susceptible to a CSRF attack. To be confident in using the SameSite approach, we'll want to know that SameSite is universally supported in browsers being used by folks out there. I'm not so sure when that'll be but, for now, we're stuck with CSRF token protection!

Top comments (7)

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Nice article. And the subject's important because, as you've pointed out, many things CORS/XSRF-related are misunderstood. If I'm being honest about it, I sometimes have to stop and check my premises to ensure that I'm not doing something wrong.

In my side projects, I've adopted a habit of issuing single-use XSRF tokens. I was thinking this would be far more secure - like having a lock that only accepts officially-issued, single-use keys. I know I've seen a few other apps that employ single-use tokens, but I notice that most apps do not do this.

What do you think of this practice? Is it overkill? Am I truly providing an increased level of protection against XSRF attacks? Or am I engaging in what I like to call "security theatre"?

Collapse
 
nas5w profile image
Nick Scialli (he/him)

Single use is probably overkill. Best practice seems to be on a per-session basis.

Collapse
 
tiguchi profile image
Thomas Werner • Edited

I was just looking into the OWASP implementation recommendations for Use of Custom Request Headers and this is what they mention there:

This defense relies on the same-origin policy (SOP) restriction that only JavaScript can be used to add a custom header, and only within its origin. By default, browsers do not allow JavaScript to make cross origin requests with custom headers.

I'm not an expert but at a glance this looks to me as if all we need is the presence of that XSRF header to make it work. It just needs to be a custom header, and the value doesn't necessarily have to be a secret.

I guess making that header a random session-based secret could be an additional layer of security in case something's not right with the browser security, be it a bug or a changed setting, or a compromised browser. Maybe there's still a bunch of older browsers out there that don't enforce SOP. But these look like problems that are more on the user's side, where they unwillingly or willingly compromised their own security.

Overall it seems regenerating the token on every request is probably not necessary. I guess it would be enough to regenerate it once on sign in, or session refresh.

Another Google search reveals a similar discussion on Stackexchange.com.

Someone asks for clarification if the following is really true and sufficient security:

The value of the header is irrelevant. This is how Jersey 1.9's CsrfProtectionFilter works and it is described in this blog post: blog.alutam.com/2011/09/14/jersey-.... The blog post also links to NSA and Stanford papers stating that the custom header itself is sufficient protection...

The accepted answer confirms that a "token-less" CSRF header would be enough today, but generating and verifying a secret should protect against future changes

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Much obliged. Those links are good reads. And they seem (to me) to confirm that, while the mere existence of the custom header may currently be "enough", it's probably not a bad idea to issue single-use tokens. Especially if that functionality has already been put in place.

Collapse
 
hansenc profile image
Chris Hansen
Collapse
 
jwp profile image
John Peters

Thanks for posting this.

Collapse
 
nas5w profile image
Nick Scialli (he/him)

Sure thing!