Stateless CSRF Protection for Express (Double Submit Cookie)
csrf-csrf is a utility package designed to provide stateless Cross-Site Request Forgery (CSRF) protection for Express applications, implementing the Double Submit Cookie Pattern. Currently at version 4.0.3, it offers a robust alternative to the deprecated `csurf` library, aiming for a simpler and more explicit configuration. Unlike session-based CSRF protection mechanisms like `csrf-sync` (which uses the Synchronizer Token Pattern), `csrf-csrf` is suited for stateless architectures, making it a distinct choice for specific application designs. The library ships with comprehensive TypeScript types (requiring TypeScript >= 3.8) and emphasizes clear implementation guidance to prevent common misconfigurations that can render CSRF protection ineffective. Development is active, with a recent major version release bringing breaking changes and improvements, and it explicitly recommends consulting upgrade guides for migration.
Common errors
-
Error: Invalid or missing CSRF token
cause The client request did not include a valid CSRF token, or `getTokenFromRequest` was unable to find it, or the token provided did not match the one expected by the server.fixEnsure the frontend sends the CSRF token (obtained from `/csrf-token` endpoint) in the correct header (`x-csrf-token`) or body field (`_csrf`) as configured by `getTokenFromRequest`. -
ReferenceError: cookieParser is not defined
cause The `cookie-parser` middleware was not correctly imported or initialized before being used in the Express application.fixAdd `import cookieParser from 'cookie-parser';` (ESM) or `const cookieParser = require('cookie-parser');` (CJS, for versions <4) and `app.use(cookieParser(...))` before `app.use(doubleCsrfProtection)`. -
TypeError: secret must be a string or array of strings
cause The `secret` option passed to the `doubleCsrf` function is either missing or not of the expected type.fixProvide a string secret to the `doubleCsrf` configuration object: `doubleCsrf({ secret: 'your-strong-secret', ... })`. -
ERR_REQUIRE_ESM
cause Attempted to use `require()` to import `csrf-csrf` in a CommonJS module, but the package (version 4+) is exclusively an ES Module.fixSwitch to ES Module syntax: `import { doubleCsrf } from 'csrf-csrf';`. Ensure your `package.json` specifies `"type": "module"` for your project, or rename files to `.mjs`.
Warnings
- breaking Version 4 introduces breaking changes. If upgrading from version 3, consult the `CHANGELOG.md` and `UPGRADING.md` guides for detailed migration steps, particularly regarding configuration options and import paths.
- gotcha The `cookie-parser` middleware MUST be registered *before* `doubleCsrfProtection`. Incorrect ordering will prevent `csrf-csrf` from accessing and setting the necessary CSRF cookies, leading to validation failures.
- gotcha The `getTokenFromRequest` configuration option requires explicit logic to retrieve the CSRF token. Avoid using fallthroughs (e.g., `||`, `??`) that might inadvertently accept tokens from multiple, potentially insecure locations, mimicking vulnerabilities found in older CSRF libraries.
- gotcha This package implements the Double Submit Cookie Pattern for *stateless* CSRF protection. If your application relies on *sessions*, it is strongly recommended to use `csrf-sync` (Synchronizer Token Pattern) instead, as it is designed for session-based CSRF protection and offers different security considerations.
- gotcha The `secret` provided to `doubleCsrf` (and any secret for `cookie-parser`) should be a strong, unique, and securely managed value, preferably loaded from environment variables. Using a hardcoded or weak secret compromises the integrity of your CSRF protection.
Install
-
npm install csrf-csrf -
yarn add csrf-csrf -
pnpm add csrf-csrf
Imports
- doubleCsrf
const { doubleCsrf } = require('csrf-csrf');import { doubleCsrf } from 'csrf-csrf'; - DoubleCsrfConfigOptions
import type { DoubleCsrfConfigOptions } from 'csrf-csrf'; - CsrfRequest
import type { CsrfRequest } from 'csrf-csrf';
Quickstart
import express from 'express';
import cookieParser from 'cookie-parser';
import { doubleCsrf } from 'csrf-csrf';
import { Request, Response } from 'express';
const app = express();
const port = 3000;
// Initialize doubleCsrf with a secret.
// For production, use a strong, securely generated secret from environment variables.
const {
doubleCsrfProtection,
generateToken,
} = doubleCsrf({
secret: process.env.CSRF_SECRET ?? 'your-very-strong-and-secret-key-that-you-must-change-in-prod-12345',
cookieName: 'x-csrf-token',
cookieOptions: {
httpOnly: true,
sameSite: 'lax', // 'Lax' or 'Strict' is recommended for CSRF protection
secure: process.env.NODE_ENV === 'production',
},
getTokenFromRequest: (req: Request) => {
// IMPORTANT: Be explicit here. Do not use fallthroughs (||, ??).
// Prioritize header, then body. If not found, return an empty string.
if (req.headers['x-csrf-token']) {
return req.headers['x-csrf-token'] as string;
}
if (req.body && req.body._csrf) {
return req.body._csrf;
}
return '';
},
});
app.use(express.json()); // For parsing application/json
app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
app.use(cookieParser(process.env.COOKIE_SECRET ?? 'your-cookie-secret-for-signing-if-needed')); // Use a separate secret for cookie-parser
// CSRF protection middleware must be after cookie-parser
app.use(doubleCsrfProtection);
// Route to get a new CSRF token for the frontend
app.get('/csrf-token', (req: Request, res: Response) => {
const csrfToken = generateToken(req);
res.json({ csrfToken });
});
// Example protected POST route
app.post('/submit-data', (req: Request, res: Response) => {
// If the doubleCsrfProtection middleware didn't throw an error,
// the request is considered valid.
res.json({ message: 'Data submitted successfully!', received: req.body });
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});