Express – Selectively Using CSURF for CSRF Protection

  • report
    Disclaimer
    Click for Disclaimer
    This Post is over a year old (first published about 3 years ago). As such, please keep in mind that some of the information may no longer be accurate, best practice, or a reflection of how I would approach the same thing today.
  • infoFull Post Details
    info_outlineClick for Full Post Details
    Date Posted:
    May. 29, 2021
    Last Updated:
    May. 29, 2021
  • classTags
    classClick for Tags

Intro

This post is going to be talking about using the csurf CSRF library with Express.

More specifically, it is going to be talking about approaches for selectively applying CSRF protection to only certain routes, and how to dynamically extract the generated token during a request without enforcing CSRF for that request. I’ll start with the approaches that don’t allow for token extraction in non-protected routes, before getting to those that do.

Approaches Without Extraction

Separate Router

Starting with the approach recommended by the official docs, one way to disable CSRF protection on a subset of routes is to use a separate Express Router and mount it to the main app / express instance before attaching the CSRF middleware.

Again, the docs have a full code example, but a quick rundown:

import csrf from 'csurf';
import express from 'express';

const app = express();

function getNonProtectedRouter() {
    const router = new express.Router();
    // Non-protected routes...
    return router;
}

// Mount non-csrf protected routes first
app.use('/nonProtectedRoutes', getNonProtectedRouter());

// Now add middleware
app.use(csrf({cookie: true}));

// protected routes...

Note: With this approach, the non-protected routes do not have access to req.csrfToken().

Passing the CSRF Middleware Via Route Argument

Often, the CSRF middleware is applied by passing it very early on to the global express instance. Something like:

const app = express();
const csrfInstance = csrf({cookie: true});

// Add all middleware
// ...
app.use(csrfInstance);

// protected routes...
app.post('/newUser', (req, res) => {
    // ...
});

However, you can also pass middleware as an argument to each route, instead of at the app level. This lets you apply it to only some routes:

const app = express();
const csrfInstance = csrf({ cookie: true });

// protected route:
app.post('/newUser', csrfInstance, (req, res) => {
    // Protected by CSRF!
});

// Unprotected route
app.post('/checkPublicUser', (req, user) => {
    // No CSRF check here!
});

Note: The unprotected routes do not have access to req.csrfToken(), similar to the separate router instance approach. Theoretically, you could however combine this approach with two separate csurf instances (like this user) – one that ignores all methods and one that doesn’t – and use the ignoring one in routes where you want to extract the token, but not enforce it.

Selectively Calling with Wrapper Middleware

Since Express allows you to create your own middleware without much setup, it is relatively pain-free to create a “wrapper” middleware that only calls the csurf middleware when you want it to, otherwise just passing the request along.

For example:

const app = express();
const csrfInstance = csrf({ cookie: true });

// Custom middleware - wrapper around csurf
app.use((req, res, next) {
    if (req.path === '/checkPublicUser') {
        // This skips CSRF - route will be unprotected
        return next();
    }

    csrfInstance(req, res, next);
});

Approaches with Extraction

Using GET and ignoreMethods

The default value for the ignoreMethods option is ['GET', 'HEAD', 'OPTIONS'], which, as you might have guessed, actually means that CSRF is not enforced for any of those request types.

So, unless you have modified this option, you can actually use any GET route without CSRF tokens being checked. The token is still generated during these requests and accessible via req.csrfToken() – in fact, that is how you are supposed to pass the token to the front-end in the first place; your GET route passes the token to a view, which embeds it into the page (usually via hidden meta or input tags).

Hooking Into Next and Ignoring Errors

This method is one that I realized could be used, but didn’t find in most code examples.

As csurf is a form of Express middleware, it shares the same interface as all Express middleware – a function that takes arguments req, res, and next. Normally, when you attach the middleware via use(), Express will automatically be calling the middleware for you on each request, with those three arguments.

However, we can also manually call the middleware, even inside of our own middleware. And, if we pass our own function as the next argument, we can intercept the result from the csurf middleware, choosing to ignore or rethrow errors it generates while checking tokens.

By combining this idea with the wrapper middleware approach, we have a way where we can run the CSRF middleware for every request, which means all requests will have tokens via req.csrfToken(), but ignore the checking of tokens when we want to.

Here it is in action:

const app = express();
const csrfInstance = csrf({ cookie: true });

// CSRF protection, with excluded route that still captures token
app.use((req, res, next) => {
    csrfInstance(req, res, (err) => {
        // Within this function, and now within all routes that follow,
        // we have access to req.csrfToken()

        if (req.path === '/nonProtected' && e.code === 'EBADCSRFTOKEN') {
            // We will ignore the specific CSRF error for this specific path
            // Notice how we are not passing err back to next()
            next();
        } else {
            // Pass request along normally
            // If there is an error from CSRF or other middleware, it will be rethrown,
            // instead of being ignored
            next(err);
        }
    });
});

// Routes go here...

📄 According to the docs, you can rely on ‘EBADCSRFTOKEN’ to be the error code.

Leave a Reply

Your email address will not be published.