csrf-sync
TypeScript icon, indicating that this package has built-in type declarations

4.2.1 • Public • Published

CSRF Sync

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

Coverage Status

Getting StartedConfigurationFAQSupport

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.

Getting Started

This section will guide you through using the default setup, which does sufficiently implement the Synchronised Token Pattern. Be sure to check the FAQ as it can help you determine whether you need CSRF protection, and if you do, whether csrf-sync is an appropriate choice, as well as provide insight on how to use it appropriately. If you would like to customise the configuration, see the configuration section, whenever you change a configuration, you should ensure you understand the impact of the change.

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. For handling the CSRF token see "How should the CSRF token be transmitted?" from the FAQ.

You may 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 or sideeffect 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);

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

errorConfig

statusCode?: number;
message?: string;
code?: string | undefined;

Optional
Default:

{
  statusCode: 403,
  message: "invalid csrf token",
  code: "EBADCSRFTOKEN"
}

Used to customise the error response statusCode, the contained error message, and it's code, the error is constructed via createHttpError. The default values match that of csurf for convenience.

getTokenFromRequest

(req: Request) => string | null | undefined;

Optional
Default:

(req: Request) => req.headers["x-csrf-token"];

This function should return the token sent by the frontend, either in the request body/payload, or from the x-csrf-token header. Do NOT return the value from a cookie in this function, this would be the same as having no CSRF protection at all, see the "How thould the csrf token be transmitted?" section of the FAQ.

getTokenFromState

(req: Request) => string | null | undefined;

Optional
Default:

(req: Request) => req.session.csrfToken;

This function should return the token from the backend state for the uniquely identified Request.

size

number;

Optional
Default:
128

The size in bytes of the generated CSRF tokens.

skipCsrfProtection

(req: Request) => boolean;

Optional - Use this option with extreme caution*

Used to determine whether CSRF protection should be skipped for the given request. If this callback is provided and the request is not in the ignoredMethods, then the callback will be called to determine whether or not CSRF token validation should be checked. If it returns true the CSRF protection will be skipped, if it returns false then CSRF protection will be checked.

* It is primarily provided to avoid the need of wrapping the csrfSynchronisedProtection middleware in your own middleware, allowing you to apply a global logic as to whether or not CSRF protection should be executed based on the incoming request. You should only skip CSRF protection for cases you are 100% certain it is safe to do so, for example, requests you have identified as coming from a native app. You should ensure you are not introducing any vulnerabilities that would allow your web based app to circumvent the protection via CSRF attacks. This option is NOT a solution for CSRF errors.

storeTokenInState

(req: Request, token?: CsrfSyncedToken) => void;

Optional
Default:

(req: Request, token: string) => {
  req.session.csrfToken = token;
}

This function should store the token in the backend state for the uniquely identified Request.

Processing as a header

When initialising 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 application/x-www-form-urlencoded content type
    // then get the token from the body.
    if (req.is("application/x-www-form-urlencoded")) {
      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

/csrf-sync/

    Package Sidebar

    Install

    npm i csrf-sync

    Weekly Downloads

    32,812

    Version

    4.2.1

    License

    ISC

    Unpacked Size

    26.2 kB

    Total Files

    7

    Last publish

    Collaborators

    • psibean