Skip to main content
Security6 min readMarch 3, 2026

CSRF Protection: Understanding Cross-Site Request Forgery and Stopping It

How CSRF attacks work, why SameSite cookies are not always sufficient, and the correct implementation of CSRF tokens for forms and single-page applications.

James Ross Jr.

James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

CSRF Protection: Understanding Cross-Site Request Forgery and Stopping It

Cross-site request forgery is one of the better-named vulnerabilities in web security. The name describes exactly what happens: a malicious site makes a request to your application using the victim's credentials, forged to appear as if it came from the victim voluntarily.

Here is the scenario. You are logged into your bank at bank.com. The session cookie is stored in your browser. You visit a malicious website, evil.com. That page contains:

<img src="https://bank.com/transfer?to=attacker&amount=5000">

Your browser tries to load that "image." It sends a GET request to bank.com/transfer, including your session cookie, because the browser always includes cookies for the target domain. If bank.com processes that transfer on a GET request, the attacker just transferred your money without you doing anything intentional.

Even with POST requests, CSRF is viable using hidden forms that auto-submit via JavaScript.

Why CSRF Works

The attack exploits the fact that browsers automatically include cookies in requests to a domain, regardless of which page initiated the request. Your bank does not know whether the request for /transfer came from your banking dashboard or from a malicious page on another domain — both will include your session cookie.

The bank's server authenticates the request (the cookie is valid), sees what looks like a legitimate transfer request, and processes it. There is no way to distinguish a CSRF attack from a legitimate user action at the cookie level alone.

The Modern Defense: SameSite Cookies

Modern browsers support the SameSite cookie attribute, which controls when browsers include cookies in cross-site requests.

res.cookie("session", sessionToken, {
  httpOnly: true,
  secure: true,
  sameSite: "strict", // or "lax"
});

sameSite: "strict" — the cookie is never sent in cross-site requests. A request from evil.com to bank.com does not include the cookie. This completely prevents CSRF.

sameSite: "lax" — the cookie is not sent in cross-site POST requests, form submissions, or requests initiated by page loading (like the <img> example above). It is sent in cross-site GET requests that result from user navigation (clicking a link). This prevents most CSRF while allowing links from other sites to work with the session.

For most applications, sameSite: "lax" is the correct default. It prevents CSRF attacks while allowing normal navigation from external sites. sameSite: "strict" is appropriate for high-security applications where breaking external links is acceptable.

The caveat: SameSite cookies depend on browser support and are not universally reliable in all environments. Older browsers do not support SameSite. Some browser extensions, proxy software, and development tools can interfere with SameSite behavior. For applications handling sensitive operations (financial transactions, account changes), SameSite cookies alone are not sufficient — combine them with CSRF tokens.

CSRF Tokens: The Belt with the Suspenders

The CSRF token pattern adds a secret value to every state-changing request that the server generates and validates. An attacker making a cross-site request cannot include the correct CSRF token because they cannot read it from your site (same-origin policy prevents cross-origin JavaScript from reading your cookies or page content).

The flow:

  1. Server generates a unique, cryptographically random token for the session
  2. Token is embedded in every HTML form and provided via a cookie or API endpoint to SPAs
  3. Every POST/PUT/PATCH/DELETE request must include the token in the request body or a header
  4. Server validates the token matches what it issued for this session
  5. If the token is missing or incorrect, the request is rejected
import { randomBytes, timingSafeEqual } from "crypto";

// Generate a CSRF token
function generateCsrfToken(): string {
  return randomBytes(32).toString("hex");
}

// Store in session
req.session.csrfToken = generateCsrfToken();

// Validate incoming token
function validateCsrfToken(req: Request): boolean {
  const sessionToken = req.session.csrfToken;
  const requestToken = req.body._csrf ?? req.headers["x-csrf-token"];

  if (!sessionToken || !requestToken) return false;

  // Use timing-safe comparison to prevent timing attacks
  const sessionBytes = Buffer.from(sessionToken);
  const requestBytes = Buffer.from(requestToken);

  if (sessionBytes.length !== requestBytes.length) return false;

  return timingSafeEqual(sessionBytes, requestBytes);
}

Using timingSafeEqual for token comparison is important. A regular === comparison short-circuits on the first different character, which can reveal information about the token through timing differences. timingSafeEqual always takes the same amount of time regardless of where the comparison fails.

Server-Side Rendered Applications

For traditional SSR applications that render HTML forms, embed the CSRF token in a hidden form field:

<form method="POST" action="/transfer">
  <input type="hidden" name="_csrf" value="{{ csrfToken }}">
  <input type="text" name="recipient">
  <input type="number" name="amount">
  <button type="submit">Transfer</button>
</form>

The CSRF token in the hidden field is served by your server. evil.com cannot read this value because cross-origin JavaScript cannot read the DOM of your pages. When the form is submitted, the token is included in the POST body and validated server-side.

Express has csurf middleware (deprecated) and its successor csrf-csrf for this:

import { doubleCsrf } from "csrf-csrf";

const { generateToken, doubleCsrfProtection } = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET!,
  cookieName: "__Host-psifi.x-csrf-token",
  cookieOptions: { secure: true, httpOnly: true },
});

app.get("/form", (req, res) => {
  const csrfToken = generateToken(req, res);
  res.render("form", { csrfToken });
});

app.use(doubleCsrfProtection);

app.post("/transfer", (req, res) => {
  // CSRF validated by middleware
  processTransfer(req.body);
});

Single-Page Applications

For SPAs using JWT or session-based authentication without cookies, CSRF is typically not applicable — the authentication credential is not automatically included in cross-site requests by the browser. JWTs stored in localStorage and submitted via Authorization header are not sent automatically by the browser in cross-origin requests.

If your SPA uses cookie-based authentication, add CSRF protection. The pattern for SPAs: the server provides the CSRF token via a separate cookie (readable by JavaScript, not httpOnly):

// Server sets a readable CSRF cookie
res.cookie("csrf-token", csrfToken, {
  httpOnly: false, // Must be readable by JavaScript
  secure: true,
  sameSite: "strict",
});

The SPA reads the token from the cookie and includes it in a custom header:

// Client reads and sends the CSRF token
function getCsrfToken(): string {
  return document.cookie
    .split("; ")
    .find((c) => c.startsWith("csrf-token="))
    ?.split("=")[1] ?? "";
}

fetch("/api/transfer", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-Token": getCsrfToken(),
  },
  credentials: "include",
  body: JSON.stringify({ recipient, amount }),
});

A cross-site request from evil.com cannot read the CSRF cookie (same-origin policy for cookie access) and therefore cannot include the correct token in the request header.

What Does Not Protect Against CSRF

SameSite cookies on their own (legacy browser support gap), checking the Content-Type header (can be spoofed with some techniques), checking the Referer header (can be absent, can be spoofed, some privacy tools strip it), and basic authentication (browser auto-includes credentials).

The correct protection is SameSite cookies combined with CSRF tokens for any application handling sensitive operations. Belt and suspenders.


If you want help implementing CSRF protection in your application or want to audit your existing protection for gaps, book a session at https://calendly.com/jamesrossjr.


Keep Reading