Back to blog

How to Prevent CSRF Attacks: Cross-Site Request Forgery Protection Guide

Even if your web application has no security holes allowing SQL injections, attackers can still exploit your users' browsers to perform unauthorized actions on their behalf. This attack vector is known as Cross-Site Request Forgery (CSRF).

CSRF attacks exploit a core browser behavior: browsers automatically attach cookies associated with a domain on every HTTP request sent to that domain, regardless of which third-party site initiated the request.

In this guide, we will analyze CSRF attack mechanics, explore modern browser protections using SameSite cookies, and implement custom CSRF token validation on your server.

Anatomy of a CSRF Attack

Imagine a user is logged into their online account at safe-bank.com. Their authentication session is stored inside a browser cookie.

While keeping the bank tab open, the user visits a malicious website, evil-hacker.com. This malicious page executes a script that submits a hidden HTML form pointing to the bank:

<!-- Hosted on evil-hacker.com -->
<form id="csrfForm" action="https://safe-bank.com/api/transfer" method="POST">
  <input type="hidden" name="amount" value="5000" />
  <input type="hidden" name="to" value="attacker_account_number" />
</form>

<script>
  // Silently submit the form on load
  document.getElementById('csrfForm').submit();
</script>

When the form submits:

  1. The browser sends the POST request to safe-bank.com.
  2. The browser automatically attaches the user's login session cookie to the request.
  3. The bank server reads the session cookie, assumes the request was authorized by the user, and transfers the funds.

Defense 1: SameSite Cookie Attribute (Natives browser barrier)

The most effective, modern defense against CSRF is configuring the SameSite attribute on session cookies. This flag controls whether cookies are sent along with requests initiated by third-party websites.

When issuing session cookies, set one of these values:

  • SameSite=Strict: The cookie is never sent in cross-site requests. If a user clicks a link on an external site pointing to your page, they will arrive logged out, as the cookie is withheld.
  • SameSite=Lax (Recommended default): The cookie is withheld on cross-site sub-requests (like images, iframes, or POST forms), but is sent when a user performs "top-level navigations" (e.g., clicking a standard link to visit your site).
  • SameSite=None: The cookie is sent in all contexts, including third-party cross-site requests. This requires the Secure flag (HTTPS only) to be set.
# Secure cookie header configuration
Set-Cookie: sessionId=xyz123; Secure; HttpOnly; SameSite=Lax;

Defense 2: Anti-CSRF Tokens (Synchronizer Token Pattern)

For absolute security (especially for state-changing operations like password changes or financial transactions), use the CSRF Token pattern.

How it Works

  1. The server generates a cryptographically secure, random token associated with the user's active session.
  2. When rendering a form or loading page templates, the server injects this token into the page (e.g., as a hidden input field or meta tag).
  3. When submitting a state-changing request (POST, PUT, DELETE), the client must include this token in the payload or HTTP headers (e.g., X-CSRF-Token).
  4. The server compares the submitted token against the token stored in the user's session. If they do not match, the request is blocked.

Since the Same-Origin Policy prevents external sites (like evil-hacker.com) from reading data from your domain, the attacker cannot read or guess the active CSRF token, causing their forged requests to fail.

Code Implementation (Node.js Express using csurf/custom logic)

Here is a conceptual server-side verification middleware:

import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.urlencoded({ extended: true }));

// Simulate session storage
const sessions: Record<string, string> = {};

// Middleware to generate and verify CSRF tokens
app.use((req, res, next) => {
  const sessionId = req.cookies?.sessionId || 'temp_session';

  // Generate token on GET requests
  if (req.method === 'GET') {
    const token = crypto.randomBytes(32).toString('hex');
    sessions[sessionId] = token;
    res.locals.csrfToken = token;
    return next();
  }

  // Verify token on state-changing requests
  if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
    const clientToken = req.body._csrf || req.headers['x-csrf-token'];
    const serverToken = sessions[sessionId];

    if (!clientToken || clientToken !== serverToken) {
      return res.status(403).send('Forbidden: Invalid CSRF Token');
    }
  }

  next();
});

Defense 3: Check Origin and Referer Headers

On state-changing requests, verify the source of the request using standard request headers:

  • Origin: Identifies the scheme, domain, and port of the requesting source.
  • Referer: Identifies the URL of the page that initiated the request.

Your server should block requests if these headers do not match your application's domain name:

const origin = req.headers['origin'];
if (origin && origin !== 'https://safe-bank.com') {
  return res.status(403).send('Forbidden: Cross-Origin request blocked');
}

Conclusion

CSRF attacks leverage browsers automatic cookie routing to execute unauthorized server actions. To protect your applications, configure session cookies with SameSite=Lax or SameSite=Strict attributes to block third-party cookie sharing, verify request origins using Origin and Referer headers, and implement cryptographic CSRF token verification middleware on all state-changing API endpoints.