A utility package to help implement stateful CSRF protection using the Synchroniser Token Pattern in express.
Getting Started • Configuration • FAQ • Support
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.
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
.
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);
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.
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.
(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.
(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.
number;
Optional
Default: 128
The size in bytes of the generated CSRF tokens.
(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.
(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.
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();
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
});
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"];
},
});
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;