CSRF Sync
A utility package to help implement stateful CSRF protection using the Synchroniser Token Pattern in express.
Dos and Don'ts •
Getting Started •
Configuration •
Support
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
import { csrfSync } from "csrf-sync";
const { csrfSync } = require("csrf-sync");
const {
invalidCsrfTokenError,
generateToken,
getTokenFromRequest,
getTokenFromState,
storeTokenInState,
revokeToken,
csrfSynchronisedProtection,
} = 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.
express.use(session);
express.get("/csrf-token", myRoute);
express.use(csrfSynchronisedProtection);
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) => {
if (isCsrfProtectionNeeded(req)) {
csrfSynchronisedProtection(req, res, next);
} else {
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);
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();
req.csrfToken(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
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;
},
getTokenFromRequest = (req) => {
return req.headers['x-csrf-token'];
},
storeTokenInState = (req, token) => {
req.session.csrfToken = token;
},
size = 128,
});
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"];
},
});
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) => {
});
Safely Using both body and header
const { csrfSynchronisedProtection } = csrfSync({
getTokenFromRequest: (req) => {
if (req.is("multipart")) {
return req.body["CSRFToken"];
}
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