Missing CSRF protection middleware

Cross-site request forgery (CSRF)

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/lax cookies, 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.

References