This Small Corner

Eric Koyanagi's tech blog. At least it's free!

Implementing the csrf-csrf package into a Node Express App

By Eric Koyanagi
Posted on

Switching to csrf-csrf (csurf deprecation)

It's really important to implement CSRF guards when developing an application. Otherwise, you open yourself to spam and abuse. For example, imagine a typical user form without CSRF protection. Any old bot can scrape the form's inputs and start crafting requests to the endpoint.

Of course you can't just check the host of incoming requests to make sure they originate from your domain. That'd be nice, wouldn't it? Maybe someday browsers will have some underlying protocol to help make this boilerplate antique, but until then, we have to be very aware of guarding our routes against CSRF exploits.

Most articles you search about adding CSRF protection in Node will tell you to implement csurf. The problem is that it's a deprecated package.

A more modern replacement is the csrf-csrf package, although I have to say it's annoying to type repeatedly. Can I just call it csrf2?

Implementing csrf-csrf globally

Personally, I'd prefer to implement this globally. Every POST has csrf validation, period. The docs don't exactly explain every step, but the setup is not too difficult. As the docs say, the module provides a few middleware methods you can re-export. From the docs:

const {
  invalidCsrfTokenError, // This is just for convenience if you plan on making your own middleware.
  generateToken, // Use this in your routes to provide a CSRF hash + token cookie and token.
  validateRequest, // Also a convenience if you plan on making your own middleware.
  doubleCsrfProtection, // This is the default CSRF protection middleware.
} = doubleCsrf(doubleCsrfOptions);

In other words, you should have a method somewhere that creates and exports these middleware. I decided to add it to my "middlewares" folder, since it provides middlewares...logical enough, right?

const { doubleCsrf } = require('csrf-csrf');

const csrf = doubleCsrf({
    getSecret: () => process.env.CSRF_SECRET,
    getTokenFromRequest: req => req.body.csrfToken,
    cookieName: process.env.NODE_ENV === 'production' ? '__Host-prod.x-csrf-token' : '_csrf',
    cookieOptions: {
      secure: process.env.NODE_ENV === 'production' // Enable for HTTPS in production
    }
});


module.exports = {
    doubleCsrfProtection: csrf.doubleCsrfProtection,
    generateToken: csrf.generateToken
};

Then I import that middleware into app.js so that the middleware is registered globally:

const { doubleCsrfProtection, generateToken } = require('./middlewares/csrf.middleware');
...
app.use(doubleCsrfProtection);
app.use((req, res, next) => {
  res.locals.csrfToken = generateToken(req, res);
  next();
});

There's few things to note about this setup.

First, the method getSecret isn't optional and will throw errors if not defined. I decided to just load a constant defined in my .env file, which maybe makes sense for you, too.

Similarly, you want to bee careful about the cookie name used. By default, it won't be valid if your site is running on a typical local environment (like "localhost:3000") because of the use of the __Host prefix by default (which is good practice for production!).

Of course you can run your local site through something like ngrok to get an HTTPS-enabled site, but I don't actually need this...so would rather handle it via configuration. If you don't pay attention to that, you'll run into errors that manifest as a simple 403 "token not valid", since it fails to set the cookie. Chrome will let you know about this in the network tab, though.

Including the CSRF token in a Form

This handles all the basics, but remember that you still need to add the CSRF token into your front-end template. I'm using handlebars, so your code will likely look different, but since we already have the token registered as a local, we can simple do this:

<input type="hidden" name="csrfToken" value="{{csrfToken}}">

Now the form submit is properly handled instead of throwing a 403 (which it will as soon as you add the global middleware).

Obligatory Wrap-up

That's it for using csrf-csrf in Node! While you can perhaps still use csurf securely, I don't think it's a great idea to use deprecated packages.

I am curious to see how the nature of CSRF protection changes over time. Perhaps someday requests submitted from "real" browser users will be signed somehow, helping devs verify that requests are coming from authentic origins and are made by actual humans.

This makes me wonder if the ultimate strategy for Chrome will be to provide more features aimed at making the Google account a "passkey", allowing signed in users to skip captchas, for example. At some point, Chrome could even require users to log in for everything except incognito browsing, labelling it as "for security", but also profiting from the host of built-in Google services.

Or perhaps that's a dim view of the future and features like CSP will become more standardized. Either way, it will come down to much tighter integration between web developers and browser vendors.

No matter how it evolves, it's important to pay close attention to CSRF flaws and exploits -- and at the very least use up-to-date modules if you aren't implementing it yourself!

« Back to Article List
Written By
Eric Koyanagi

I've been a software engineer for over 15 years, working in both startups and established companies in a range of industries from manufacturing to adtech to e-commerce. Although I love making software, I also enjoy playing video games (especially with my husband) and writing articles.

Article Home | My Portfolio | My LinkedIn
© All Rights Reserved