Skip to content

A utility package to help implement stateful CSRF protection using the Synchroniser Token Pattern in express.

License

Notifications You must be signed in to change notification settings

Psifi-Solutions/csrf-sync

Repository files navigation

CSRF Sync

A utility package to help implement stateful CSRF protection using the Synchroniser Token Pattern in express.

Dos and Don'tsGetting StartedConfigurationSupport

Background

This module intends to provide the necessary pieces required to implement CSRF protection using the Synchroniser Token Pattern. This means you will require server side state, if you require stateless CSRF protection, please see csrf-csrf for the Double-Submit Cookie Pattern.

Since csurf has been deprecated I struggled to find alternative solutions that were accurately implemented and configurable, so I decided to write my own! A lot of CSRF protection based packages often try to provide a full solution, or they only provide the Double-Submit Cookie method, and in doing so, they become rather complicated to configure. So much so, that your configuration alone could render the protection completely useless.

This is why csrf-sync aims to provide a single and targeted implementation to simplify it's use.

Dos and Don'ts

  • Do read the OWASP - Cross-Site Request Forgery Prevention Cheat Sheet
  • Do read the OWASP - Session Management Cheat Sheet
  • Do join the Discord server and ask questions in the psifi-support channel if you need help.
  • Do make sure you do not compromise your security by not following OWASP practices.
  • Do not transmit your CSRF token by cookies.
  • Do not include your CSRF tokens in any log output.
  • Do not use the same unauthenticated session for a user after they have authenticated. Make sure you destroy the session and create a new one. If they logout, destroy the session and create a new one. Keep in mind any generated token will be lost once a session is destroyed.

Getting Started

This section will guide you through using the default setup, which does sufficiently implement the Synchronised Token Pattern. If you'd like to customise the configuration, see the configuration section.

You will need to be using express-session (or a session middleware which provides a request.session property). this utility will add a csrfToken property to request.session.

npm install express express-session csrf-sync
// ESM
import { csrfSync } from "csrf-sync";
// CommonJS
const { csrfSync } = require("csrf-sync");
const {
  invalidCsrfTokenError, // This is just for convenience if you plan on making your own middleware.
  generateToken, // Use this in your routes to generate, store, and get a CSRF token.
  getTokenFromRequest, // use this to retrieve the token submitted by a user
  getTokenFromState, // The default method for retrieving a token from state.
  storeTokenInState, // The default method for storing a token in state.
  revokeToken, // Revokes/deletes a token by calling storeTokenInState(undefined)
  csrfSynchronisedProtection, // This is the default CSRF protection middleware.
} = csrfSync();

This will extract the default utilities, you can configure these and re-export them from your own module. You should only transmit your token to the frontend as part of a response payload, do not include the token in response headers or in a cookie.

This means you will need to create your own route(s) for generating and retrieving a token. For example, a JSON endpoint which you can call before making form submissions:

const myRoute = (req, res) => res.json({ token: generateToken(req) });
const myProtectedRoute = (req, res) =>
  res.json({ unpopularOpinion: "Game of Thrones was amazing" });

You can also put the token into the context of a templated HTML response. Note in this case, the route is a get request, and these request types are not protected (ignored request method), as they do not need to be protected so long as the route is not exposing any sensitive actions.

// Make sure your session middleware is registered before these
express.use(session);
express.get("/csrf-token", myRoute);
express.use(csrfSynchronisedProtection);
// Anything registered after this will be considered "protected"

You can also protect your routes on a case-to-case basis:

app.get("/secret-stuff", csrfSynchronisedProtection, myProtectedRoute);

Or you can conditionally wrap the middleware yourself, like so (basic example):

const myCsrfProtectionMiddleware = (req, res, next) => {
  // Some method to determine whether we want CSRF protection to apply
  if (isCsrfProtectionNeeded(req)) {
    // protect with CSRF
    csrfSynchronisedProtection(req, res, next);
  } else {
    // Don't protect with CSRF
    next();
  }
};
express.use(myCsrfProtectionMiddleware);

And now this will only require a CSRF token to be present for requests where isCsrfProtectionNeeded(req) evaluates to false.

Once a route is protected, you will need to include the most recently generated token in the x-csrf-token request header, otherwise you'll receive a 403 - ForbiddenError: invalid csrf token.

generateToken

By default if a token already exists on the session object, generateToken will not overwrite it, it will simply return the existing token. If you wish to force a token generation, you can use the second parameter:

generateToken(req, true); // This will force a new token to be generated, even if one already exists

Instead of importing and using generateToken, you can also use req.csrfToken any time after the csrfSynchronisedProtection middleware has executed on your incoming request.

req.csrfToken(); // same as generateToken(req) and generateToken(req, false);
req.csrfToken(true); // same as generateToken(req, true);

revokeToken

By default tokens will NOT be revoked, if you want or need to revoke a token you should use this method to do so. Note that if you call generateToken with overwrite set to true, this will revoke the any existing token and only the new one will be valid.

Configuration

Processing as a header

When creating your csrfSync, you have a few options available for configuration, all of them are optional and have sensible defaults (shown below).

const csrfSyncProtection = csrfSync({
  ignoredMethods = ["GET", "HEAD", "OPTIONS"],
  getTokenFromState = (req) => {
    return req.session.csrfToken;
  }, // Used to retrieve the token from state.
  getTokenFromRequest = (req) =>  {
    return req.headers['x-csrf-token'];
  }, // Used to retrieve the token submitted by the request from headers
  storeTokenInState = (req, token) => {
    req.session.csrfToken = token;
  }, // Used to store the token in state.
  size = 128, // The size of the generated tokens in bits
});

// NOTE THE VALUES ABOVE ARE THE DEFAULTS.
// THE ABOVE IS THE SAME AS DOING:

const csrfSyncProtection = csrfSync();

Processing as a form

If you intend to use this module to protect user submitted forms, then you can use generateToken to create a token and pass it to your view, likely via template variables. Then using a hidden form input such as the example from the Cheat Sheet.

<form action="/transfer.do" method="post">
  <input
    type="hidden"
    name="CSRFToken"
    value="OWY4NmQwODE4ODRjN2Q2NTlhMmZlYWEwYzU1YWQwMTVhM2JmNGYxYjJiMGI4MjJjZDE1ZDZMGYwMGEwOA=="
  />
  [...]
</form>

Upon form submission a csrfSync configured as follows can be used to protect the form.

const { csrfSynchronisedProtection } = csrfSync({
  getTokenFromRequest: (req) => {
    return req.body["CSRFToken"];
  }, // Used to retrieve the token submitted by the user in a form
});

If using this with something like express you would need to provide/configure body parsing middleware before the CSRF protection.

If doing this per route, you would for example:

app.post("/route/", csrfSynchronisedProtection, async (req, res) => {
  //process the form as we passed CSRF
});

Safely Using both body and header

const { csrfSynchronisedProtection } = csrfSync({
  getTokenFromRequest: (req) => {
    // If the incoming request is a multipart content type
    // then get the token from the body.
    if (req.is("multipart")) {
      return req.body["CSRFToken"];
    }
    // Otherwise use the header for all other request types
    return req.headers["x-csrf-token"];
  },
});

Using asynchronously

csrf-sync itself will not support promises or async, however there is a way around this. If your csrf token is stored externally and needs to be retrieved asynchronously, you can register an asynchronous middleware first, which exposes the token.

(req, res, next) => {
  getCsrfTokenAsync(req)
    .then((token) => {
      req.asyncCsrfToken = token;
      next();
    })
    .catch((error) => next(error));
};

And in this example, your `getTokenFromRequest` would look like this:

(req) => req.asyncCsrfToken;

Support

  • Join the Discord and ask for help in the psifi-support channel.
  • Pledge your support through the Patreon

Buy Me A Coffee

About

A utility package to help implement stateful CSRF protection using the Synchroniser Token Pattern in express.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published