Description
CSRF exploits a user's authenticated session to send unwanted state-changing requests, such as POST, PUT, PATCH, or DELETE. If an Express application uses cookie or session authentication without adequate CSRF defenses, an attacker may lure a user to a malicious page that submits a form or AJAX request. This can cause account changes, payments, or settings updates with the user's session privileges.
Potential impact
- Unauthorized actions such as changing an email address or password, or transferring funds
- Unintended changes to account information or settings, potentially exposing sensitive data
- Financial or operational losses through unauthorized payments, orders, or permission changes
Remediation
- For cookie or session authentication, use maintained CSRF middleware or the framework's built-in protection globally (
app.use) or on relevant routes. Check maintenance status before adoption. - Protect state-changing POST, PUT, PATCH, and DELETE routes.
- Send server-generated CSRF tokens in hidden form inputs or AJAX headers such as
X-CSRF-Token, and validate them on the server. - Do not change state through GET requests.
- Add
SameSite=strict/laxcookies, Referer/Origin validation for sensitive endpoints, and CAPTCHA or reauthentication where appropriate.
Examples
Before
javascript
const express = require("express");
const session = require("express-session");
const app = express();
// Run only behind one trusted TLS proxy; do not expose the application directly.
app.set("trust proxy", 1);
app.use(express.urlencoded({ extended: false }));
const sessionSecret = process.env.SESSION_SECRET;
if (!sessionSecret || sessionSecret.length < 32) {
throw new Error("SESSION_SECRET must contain at least 32 characters");
}
app.use(
session({
secret: sessionSecret,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, sameSite: "lax" },
})
);
// Before: session authentication without CSRF protection middleware.
app.post("/account/email", (req, res) => {
const userId = req.session.userId;
const newEmail = req.body.email;
// ... Update the email address in the database.
res.send("updated");
});
app.listen(3000);
After
javascript
const express = require("express");
const session = require("express-session");
const { csrfSync } = require("csrf-sync");
const { csrfSynchronisedProtection, generateToken } = csrfSync({
getTokenFromRequest(req) {
if (req.is("application/x-www-form-urlencoded")) {
return req.body.csrfToken;
}
return req.headers["x-csrf-token"];
},
});
const app = express();
// Run only behind one trusted TLS proxy; do not expose the application directly.
app.set("trust proxy", 1);
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
const sessionSecret = process.env.SESSION_SECRET;
if (!sessionSecret || sessionSecret.length < 32) {
throw new Error("SESSION_SECRET must contain at least 32 characters");
}
app.use(
session({
secret: sessionSecret,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, sameSite: "lax" },
})
);
// Apply CSRF token validation to all state-changing routes.
app.use(csrfSynchronisedProtection);
// Use the hidden `csrfToken` form field or the `x-csrf-token` header for JSON requests.
app.get("/account/email", (req, res) => {
res.json({ csrfToken: generateToken(req) });
});
// After: reject requests without a token.
app.post("/account/email", (req, res) => {
const userId = req.session.userId;
const newEmail = req.body.email;
// ... Update the database.
res.send("updated");
});
app.listen(3000);
Explanation:
- Before: A trusted TLS proxy, a sufficiently long session secret from the environment, and secure cookie attributes do not replace CSRF defenses. Without token validation, same-site attacks may still forge state-changing requests.
- After: Apply the selected CSRF middleware globally alongside the same proxy and session settings to validate server-issued tokens. This example assumes a single trusted TLS proxy. State-changing requests without a valid CSRF token are rejected.