/csrf-csrf

A utility package to help implement stateless CSRF protection using the Double Submit Cookie Pattern in express.

Primary LanguageTypeScriptOtherNOASSERTION

Double CSRF

A utility package to help implement stateless CSRF protection using the Double Submit Cookie Pattern in express.

Dos and Don'tsGetting StartedConfigurationSupport

Background

This module provides the necessary pieces required to implement CSRF protection using the Double Submit Cookie Pattern. This is a stateless CSRF protection pattern, if you are using sessions and would prefer a stateful CSRF strategy, please see csrf-sync for the Synchroniser Token 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! Thanks to NextAuth as I referenced their implementation. From experience CSRF protection libraries tend to complicate their configuration, and if misconfigured, can render the protection completely useless.

This is why csrf-csrf aims to provide a simple 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 - Secrets Management Cheat Sheet
  • Do follow the recommendations when configuring csrf-double.
  • Do join the Discord server and ask questions in the psifi-support channel if you need help.
  • Do follow fastify/csrf-protection recommendations for secret security.
  • Do keep secure and signed as true in production.
  • Do make sure you do not compromise your security by not following best practices.
  • Do not use the same secret for csrf-csrf and cookie-parser.
  • Do not transmit your CSRF token by cookies.
  • Do not expose your CSRF tokens or hash in any log output or transactions other than the CSRF exchange.
  • Do not transmit the token hash by any other means.

Getting Started

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

You will need to be using cookie-parser and the middleware should be registered before Double CSRF. This utility will set a cookie containing a hash of the csrf token and provide the non-hashed csrf token so you can include it within your response.

npm install cookie-parser csrf-csrf
// ESM
import { doubleCsrf } from "csrf-csrf";

// CommonJS
const { doubleCsrf } = require("csrf-csrf");
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 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);

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, and do not transmit the token hash by any other means.

To create a route which generates a CSRF token and hash cookie:

const myRoute = (request, response) => {
  const csrfToken = generateToken(response);
  // You could also pass the token into the context of a HTML response.
  res.json({ csrfToken });
};
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. Just make sure you register this route before registering the middleware so you don't block yourself from getting a token.

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

By default, any request that are not GET, HEAD, or OPTIONS methods will be protected. You can configure this with the ignoredMethods option.

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

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

Once a route is protected, you will need to ensure the hash cookie is sent along with the request and by default you will need to include the generated token in the x-csrf-token header, otherwise you'll receive a `403 - ForbiddenError: invalid csrf token`. If your cookie is not being included in your requests be sure to check your withCredentials and CORS configuration.

Configuration

When creating your doubleCsrf, you have a few options available for configuration, the only required option is getSecret, the rest have sensible defaults (shown below).

const doubleCsrfUtilities = doubleCsrf({
  getSecret: () => "Secret", // A function that optionally takes the request and returns a secret
  cookieName: "__Host-psifi.x-csrf-token", // The name of the cookie to be used, recommend using Host prefix.
  cookieOptions: {
    httpOnly = true,
    sameSite = "lax",  // Recommend you make this strict if posible
    path = "/",
    secure = true,
    ...remainingCOokieOptions // Additional options supported: domain, maxAge, expires
  },
  size: 64, // The size of the generated tokens in bits
  ignoredMethods: ["GET", "HEAD", "OPTIONS"], // A list of request methods that will not be protected.
  getTokenFromRequest: (req) => req.headers["x-csrf-token"], // A function that returns the token from the request
});

Sessions

If you plan on using express-session then please ensure your cookie-parser middleware is registered after express-session, as express session parses it's own cookies and may cionflict.

getSecret

(request: Request) => string;

This should return a secret key for hashing, using a hard coded string return works:

() => "my key";

However it is highly recommend you implement some rotating secret key so that tokens become invalidated after a certain period of time. For example, you could use sessions, or some server side state attached to the request (via middleware). You could then have some external means of updating and rotating what your getSecret returns and you could then use that:

(req) => req.secret;
// or
(req) => req.session.secret;
// or some other externally rotated secret key

Support

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