← Back to Blog

CSRF Protection Explained: How to Prevent Cross-Site Request Forgery

A CSRF attack can drain a bank account, change an email address, or delete data - all without the victim clicking anything malicious directly. Understanding how CSRF exploits browser behavior is the first step to building effective defenses. This guide covers the attack in depth and every practical mitigation technique.

The Problem: Browsers Send Cookies Automatically

CSRF (Cross-Site Request Forgery) exploits a fundamental browser behavior: the browser automatically sends cookies for a domain with every request to that domain, regardless of which page initiated the request.

This means if you are logged into bank.example.com (which stores your session in a cookie), and you visit a malicious page at evil.com, that page can make the browser send a request to bank.example.com and your session cookie will be included automatically - making the server think the request came from you.

How a CSRF Attack Works: Step by Step

Here is a concrete example. Suppose your bank has an endpoint:

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: session=abc123

amount=1000&to_account=12345

The bank's server sees the session cookie, recognizes you as a legitimate user, and processes the transfer. Now consider this malicious page:

<!-- evil.com/steal.html -->
<html>
<body>
  <!-- Auto-submitting form on page load -->
  <form action="https://bank.example.com/transfer" method="POST" id="f">
    <input type="hidden" name="amount" value="10000">
    <input type="hidden" name="to_account" value="attacker_account">
  </form>
  <script>document.getElementById('f').submit();</script>
</body>
</html>

When you visit evil.com/steal.html while logged into your bank:

  1. The page auto-submits the form to bank.example.com/transfer
  2. Your browser includes your session cookie with the request
  3. The bank server sees a valid session and processes the $10,000 transfer
  4. You have been robbed without clicking anything suspicious

The same technique works with images, iframes, and JavaScript fetch() calls (for GET requests and some POST requests). CSRF can also target:

  • Email address changes (account takeover)
  • Password changes
  • Admin actions (creating users, modifying settings)
  • Social media posts and follows
  • E-commerce orders

Defense Method 1: CSRF Tokens (Synchronizer Token Pattern)

The most widely used CSRF defense. The server generates a unique, unpredictable token for each session (or each request), embeds it in forms and AJAX headers, and validates it on every state-changing request. An attacker on a different origin cannot read the token (due to the Same-Origin Policy), so they cannot forge a valid request.

Implementation in HTML forms

<!-- Server embeds the CSRF token in every form -->
<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="f3a2b9c7d1e4...">
  <input type="number" name="amount">
  <button type="submit">Transfer</button>
</form>

Implementation in AJAX requests

// Include CSRF token in AJAX request header
// Token is read from a meta tag or cookie (not HttpOnly)

// Reading from meta tag (set by server in HTML head)
const token = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': token,       // Custom header that cross origin requests cannot set
  },
  body: JSON.stringify({ amount: 1000, to: 'account123' })
});

Server-side validation (Node.js/Express example)

// Install: npm install csurf (deprecated) or use a modern alternative
// Modern approach: manual token generation and validation

const crypto = require('crypto');

function generateCsrfToken(session) {
  if (!session.csrfSecret) {
    session.csrfSecret = crypto.randomBytes(32).toString('hex');
  }
  return crypto
    .createHmac('sha256', session.csrfSecret)
    .update(session.id)
    .digest('hex');
}

function validateCsrfToken(session, token) {
  const expected = generateCsrfToken(session);
  // Use timingSafeEqual to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(token, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

// Middleware
function csrfProtect(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next(); // Safe methods don't need CSRF protection
  }
  const token = req.headers['x-csrf-token'] || req.body.csrf_token;
  if (!token || !validateCsrfToken(req.session, token)) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }
  next();
}

Defense Method 2: SameSite Cookie Attribute

The SameSite cookie attribute is the most impactful CSRF defense added to browsers in recent years. It controls when the browser sends a cookie along with cross site requests.

# SameSite=Strict: cookie NEVER sent on cross-site requests
# Safest, but breaks OAuth flows and external links (user appears logged out)
Set-Cookie: session=abc123; SameSite=Strict; HttpOnly; Secure

# SameSite=Lax: cookie sent on top-level navigations (clicking links)
# but NOT on cross-site form POSTs, iframes, or AJAX
# Default in Chrome since 2020 - this is what most sites should use
Set-Cookie: session=abc123; SameSite=Lax; HttpOnly; Secure

# SameSite=None: always send (old behavior) - requires Secure flag
# Required for cross-site contexts (embedded widgets, payment iframes)
Set-Cookie: embed_session=xyz; SameSite=None; Secure

SameSite=Lax is the recommended default. It blocks the most common CSRF attack vectors (cross site form POSTs) while preserving user experience for site navigations from external links. Chrome, Firefox, and Safari all default to Lax for cookies that do not specify SameSite.

SameSite=Lax alone does not fully replace CSRF tokens. Edge cases exist: top-level navigation GET requests can be CSRF'd (though they should be safe methods), and some attack scenarios use cross site requests that qualify as "top-level navigation." Defense in depth recommends both SameSite and CSRF tokens.

Defense Method 3: Double-Submit Cookie Pattern

An alternative to server side token storage. The server sets a random value in a non-HttpOnly cookie. The JavaScript on the page reads this cookie and includes the value in every state-changing request (as a header or body parameter). The server verifies that the cookie value matches the request value. An attacker cannot read the cookie cross origin, so they cannot forge the matching value.

# Server sets a non-HttpOnly CSRF cookie (so JS can read it)
Set-Cookie: csrf_double_submit=randomvalue123; SameSite=Strict; Secure
# Note: NOT HttpOnly - JS must be able to read it

# JavaScript reads the cookie and sends it as a header
function getCookie(name) {
  return document.cookie.split(';')
    .find(c => c.trim().startsWith(name + '='))
    ?.split('=')[1];
}

fetch('/api/action', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': getCookie('csrf_double_submit'),
  },
  credentials: 'include', // Sends cookies
  body: JSON.stringify(data)
});

The server verifies: request header X-CSRF-Token value == cookie csrf_double_submit value. Since the attacker's page cannot read the cookie (SameSite prevents it being sent, or the attacker is cross origin and cannot read cookies), they cannot forge matching values.

Defense Method 4: Checking Origin and Referer Headers

On state-changing requests, check the Origin header (preferred) or Referer header to verify the request originated from your own domain. This is a secondary defense, not a primary one - these headers can occasionally be absent (some privacy-focused browsers strip them).

function isValidOrigin(req) {
  const origin = req.headers.origin;
  const referer = req.headers.referer;

  const allowedOrigins = [
    'https://app.example.com',
    'https://www.example.com'
  ];

  // Check Origin header (preferred)
  if (origin) {
    return allowedOrigins.includes(origin);
  }

  // Fallback to Referer
  if (referer) {
    return allowedOrigins.some(allowed => referer.startsWith(allowed));
  }

  // No origin header at all - conservative: block
  return false;
}

Generate Secure Random Tokens

Use our free Hash Generator to create cryptographically secure random values for CSRF tokens, API keys, and secrets. Runs entirely in your browser.

Open Hash Generator →

Framework-Level CSRF Protection

Most modern web frameworks include CSRF protection built in. Always enable it rather than rolling your own.

Django (Python)

# settings.py - CSRF middleware is enabled by default
MIDDLEWARE = [
    'django.middleware.csrf.CsrfViewMiddleware',  # Already included
    ...
]

# In templates: {% csrf_token %} generates the hidden input
# For AJAX: read the csrftoken cookie and send as X-CSRFToken header

# Exempt an API view (use carefully, only for token-authenticated APIs)
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def my_api_view(request):
    ...

Rails (Ruby)

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # protect_from_forgery is ON by default in Rails
  protect_from_forgery with: :exception

  # For API-only controllers using token auth, use:
  protect_from_forgery with: :null_session
end

# In forms: <%= form_with ... %> automatically includes CSRF token
# Rails sets an authenticity_token hidden field in all forms

Laravel (PHP)

<!-- In Blade templates -->
<form method="POST" action="/transfer">
    @csrf
    <!-- Generates: <input type="hidden" name="_token" value="..."> -->
</form>

<!-- For AJAX: include meta tag in <head> -->
<meta name="csrf-token" content="{{ csrf_token() }}">

// JavaScript:
axios.defaults.headers.common['X-CSRF-TOKEN'] =
    document.querySelector('meta[name="csrf-token"]').getAttribute('content');

// VerifyCsrfToken middleware is in the default middleware stack
// Exemptions: app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
    'api/*', // Exclude API routes (use token auth instead)
];

CSRF and REST APIs: When Tokens Are Not Needed

Pure REST APIs that use token based authentication (JWT in the Authorization header, API keys in custom headers) are generally immune to CSRF. Here is why: CSRF relies on the browser automatically including credentials (cookies) with cross site requests. A cross site request cannot include a custom Authorization header value that the attacker's page cannot read (due to Same-Origin Policy). If your API requires Authorization: Bearer <token> and the token lives in memory (not in a cookie), CSRF is not a threat.

However, if your API relies on cookie based session authentication, CSRF protection is needed even for REST APIs. This is a common mistake: developers assume "it's a REST API, it's safe" without checking whether session cookies are involved.

Frequently Asked Questions

Does HTTPS protect against CSRF?

No. HTTPS encrypts the communication channel and prevents eavesdropping and man in the middle attacks, but it does not prevent CSRF. The attack does not need to intercept any traffic - it simply causes the victim's authenticated browser to make a legitimate-looking request to the target site. The request arrives over HTTPS just like a real request would. CSRF is about the origin of the request, not the encryption of the channel.

Can CSRF steal data (read responses)?

Classic CSRF cannot read the response from the forged request due to the Same-Origin Policy. The attacker can cause the server to take an action (transfer money, change email) but cannot read the response body to steal data. However, if combined with other vulnerabilities (like an XSS vulnerability on the target domain), an attacker could read responses. Some historical CSRF attacks used browser side-channel techniques to infer whether a request succeeded, but these are exceptional cases.

What is the difference between XSS and CSRF?

They exploit different trust relationships. XSS (Cross-Site Scripting) exploits the user's trust in the target website: an attacker injects malicious scripts into the website that run in the user's browser with the site's privileges. CSRF exploits the website's trust in the user's browser: an attacker tricks the user's authenticated browser into sending requests the server treats as legitimate. A common mnemonic: XSS = the site trusts the user's browser; CSRF = the server trusts the user's browser's cookies. XSS can be used to steal CSRF tokens, making XSS prevention also important for CSRF defense.

Do single-page applications (SPAs) need CSRF protection?

SPAs that use JWT tokens stored in memory (not cookies) and send them via the Authorization header are immune to CSRF - the Authorization header cannot be set by cross site requests. SPAs that authenticate via session cookies (even if built as a SPA) need CSRF protection. The common pattern for SPAs: use a non-HttpOnly cookie to store the CSRF token, read it in JavaScript, and send it as a custom request header (X-CSRF-Token). This is the double submit cookie pattern and is safe because custom headers cannot be set cross origin.

Is checking the Content-Type sufficient CSRF protection?

No, not by itself. Early guidance suggested that requiring Content-Type: application/json was CSRF-safe because HTML forms cannot send JSON content type. However, this assumption has eroded: some older browser versions, Flash (now deprecated), and various edge cases allowed cross origin requests with application/json. Additionally, some APIs accept multiple content types. Use CSRF tokens or SameSite cookies as your primary defense - content type checking is at best a secondary signal.

The Bottom Line

CSRF is one of the most exploited web vulnerabilities precisely because it is invisible to the victim. The defense strategy in 2026:

  1. Set SameSite=Lax (or Strict) on all session cookies - this is now the browser default but should be explicit
  2. Add CSRF tokens to all state-changing form submissions and AJAX requests
  3. Validate the Origin header as a secondary check
  4. Use your framework's built-in CSRF protection rather than implementing from scratch
  5. For pure API endpoints with Authorization header auth: no CSRF token needed, but ensure the endpoint truly requires the header and does not fall back to cookie auth

Use our free tool here → Hash Generator to generate cryptographically secure random values suitable for CSRF tokens, nonces, and other security-sensitive random strings.

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.