CSRF Attacks Explained: Tokens, SameSite Cookies & Modern Defenses

SCRs Team
March 10, 2026
14 min read
338 words
Share

What Is CSRF and Why Does It Still Work?

Cross-Site Request Forgery (CSRF) tricks a user's browser into making unintended requests to a site where they're already authenticated. The browser automatically includes cookies — so the server thinks it's a legitimate request.

How a CSRF Attack Works

1. User logs into bank.com (session cookie set)
2. User visits evil.com (in another tab)
3. evil.com contains hidden form:

<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker-account" />
  <input type="hidden" name="amount" value="10000" />
</form>
<script>document.forms[0].submit();</script>

4. Browser sends the request WITH the user's bank.com session cookie
5. Bank processes the transfer — it looks legitimate

This works because the browser automatically attaches cookies to any request matching the cookie's domain, regardless of which website initiated the request.


CSRF Attack Variants

1. Form-Based CSRF (Classic)

<!-- Hidden auto-submitting form -->
<body onload="document.getElementById('csrf').submit()">
  <form id="csrf" action="https://target.com/api/change-email" method="POST">
    <input type="hidden" name="email" value="attacker@evil.com" />
  </form>
</body>

2. Image Tag CSRF (GET Requests)

<!-- Triggers GET request silently -->
<img src="https://target.com/api/delete-account?confirm=true" width="0" height="0" />

3. XMLHttpRequest CSRF

// Works if CORS allows it or for simple requests
fetch('https://target.com/api/change-password', {
  method: 'POST',
  credentials: 'include', // Sends cookies
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: 'new_password=hacked123'
});

4. JSON-Based CSRF

<!-- Exploiting content-type bypass -->
<form action="https://target.com/api/update-profile" method="POST" enctype="text/plain">
  <input name='{"role":"admin","ignore":"' value='"}' />
</form>

This sends: {"role":"admin","ignore":"="} — valid JSON that changes the user's role.


Defense #1: CSRF Tokens (Synchronizer Pattern)

The most reliable defense. Generate a random token, embed it in forms, and verify on the server.

// Server: Generate CSRF token
import crypto from 'crypto';

function generateCsrfToken(session: Session): string {
  const token = crypto.randomBytes(32).toString('hex');
  session.csrfToken = token;
  return token;
}

// Server: Verify CSRF token
function verifyCsrfToken(req: Request, session: Session): boolean {
  const token = req.body._csrf || req.headers['x-csrf-token'];
  if (!token || token !== session.csrfToken) {
    return false;
  }
  // Rotate token after use (one-time use)
  session.csrfToken = crypto.randomBytes(32).toString('hex');
  return true;
}

// Express middleware
function csrfProtection(req: Request, res: Response, next: NextFunction) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
    return next(); // Safe methods don't need CSRF protection
  }
  
  if (!verifyCsrfToken(req, req.session)) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }
  next();
}
<!-- Client: Include token in forms -->
<form method="POST" action="/api/transfer">
  <input type="hidden" name="_csrf" value="{{csrfToken}}" />
  <input name="amount" />
  <button type="submit">Transfer</button>
</form>

Defense #2: SameSite Cookies

// Set SameSite attribute on session cookies
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',   // Blocks cross-site POST requests
  // sameSite: 'strict' // Blocks ALL cross-site requests (breaks OAuth flows)
  maxAge: 3600000,
});
SameSite ValueCross-Site POSTCross-Site GETOAuth Flows
None✅ Sent (vulnerable)✅ Sent✅ Works
Lax (default)❌ Blocked✅ Sent (top-level)✅ Works
Strict❌ Blocked❌ Blocked❌ Breaks

Why SameSite Isn't Enough Alone

  1. Lax allows GET requests — if your app has state-changing GET endpoints, they're still vulnerable
  2. Subdomain bypass — attacker on sub.target.com can still send cookies
  3. Browser bugs — Some older browsers don't enforce SameSite properly
  4. WebSocket bypass — SameSite doesn't apply to WebSocket connections

Always combine SameSite cookies with CSRF tokens for defense-in-depth.


For stateless APIs where you can't store tokens in sessions:

// Set a random CSRF cookie
const csrfValue = crypto.randomBytes(32).toString('hex');
res.cookie('csrf', csrfValue, { 
  httpOnly: false, // Client needs to read this
  secure: true, 
  sameSite: 'lax' 
});

// Middleware: Verify cookie value matches header value
function doubleSubmitCheck(req: Request, res: Response, next: NextFunction) {
  const cookieValue = req.cookies.csrf;
  const headerValue = req.headers['x-csrf-token'];
  
  if (!cookieValue || cookieValue !== headerValue) {
    return res.status(403).json({ error: 'CSRF validation failed' });
  }
  next();
}
// Client: Read cookie and send as header
const csrfToken = document.cookie
  .split('; ')
  .find(c => c.startsWith('csrf='))
  ?.split('=')[1];

fetch('/api/transfer', {
  method: 'POST',
  headers: { 'X-CSRF-Token': csrfToken },
  body: JSON.stringify({ amount: 100 }),
});

Framework-Specific CSRF Protection

Next.js Server Actions

Next.js Server Actions have built-in CSRF protection via the Next-Action header — but verify this is enforced in your version.

Django

# Built-in — just use the template tag
{% csrf_token %}

# For AJAX, read the csrftoken cookie and send as X-CSRFToken header

Express

import { doubleCsrf } from 'csrf-csrf';

const { doubleCsrfProtection, generateToken } = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET!,
  cookieName: '__csrf',
  cookieOptions: { secure: true, sameSite: 'lax' },
});

app.use(doubleCsrfProtection);

CSRF Testing Checklist

  • All state-changing endpoints require CSRF tokens
  • Session cookies have SameSite=Lax or Strict
  • No state-changing operations on GET requests
  • CSRF tokens are random, unique per session, and verified server-side
  • Custom headers required for API calls (X-Requested-With, X-CSRF-Token)
  • Content-Type validation rejects unexpected text/plain submissions
  • WebSocket connections verify origin header
Editorial standards

Published by SecureCodeReviews

This article is part of our original AI security and cybersecurity content library. We show publish and update dates, keep company and policy pages public, and update important guidance when material changes affect readers.

Named author: SCRs Team
Published: Mar 10, 2026
Update status: current publication version

Questions or corrections?

Review our editorial standards, learn more about the company, or contact us if a page needs clarification.

AI Security Audit

Planning an AI feature launch or security review?

We assess prompt injection paths, data leakage, tool use, access control, and unsafe AI workflows before they become production problems.

Manual review for agent, prompt, and retrieval attack paths
Actionable remediation guidance for your AI stack
Coverage for LLM apps, MCP integrations, and internal AI tools

Talk to SecureCodeReviews

Get a scoped review path fast

Manual review
Actionable fixes
Fast turnaround
Security-focused

Advertisement