Skip to content
AstroPaper
Go back

Cross-Site Request Forgery (CSRF)

Updated:
Edit page

Cross-Site Request Forgery (CSRF) — Ultimate Deep-Dive Guide

A complete engineering guide from beginner concepts to browser-security-engine-level mental models and enterprise-scale secure web architecture.


Table of Contents

Open Table of Contents

1. Big Picture

What CSRF Actually Is

Cross-Site Request Forgery (CSRF) is an attack where a malicious website causes a victim’s browser to perform an unwanted action on a trusted site where the victim is authenticated. The attack exploits the browser’s automatic credential attachment behavior — browsers automatically include cookies (session tokens, auth tokens) with every request to a domain, regardless of which site initiated the request.

The core problem: The server cannot distinguish between a request the user intentionally made and a request a malicious site tricked the user’s browser into making.

Why CSRF Exists

CSRF exists because of a fundamental design decision in web browsers:

  1. Browsers automatically attach cookies to requests based on the destination domain, not the origin of the request
  2. HTTP is stateless — servers rely on cookies/sessions to maintain authentication state
  3. The web was designed for document linking — any page can link/submit to any other page
  4. Trust is domain-based — cookies are scoped to domains, not to specific pages or origins
┌─────────────────────────────────────────────────────────────────┐
│                    CSRF Mental Model                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│   User authenticates to bank.com                                 │
│        │                                                          │
│        ▼                                                          │
│   Browser stores session cookie: bank.com → session=abc123       │
│        │                                                          │
│        ▼                                                          │
│   User visits evil.com (in another tab)                          │
│        │                                                          │
│        ▼                                                          │
│   evil.com contains: <form action="bank.com/transfer" ...>       │
│        │                                                          │
│        ▼                                                          │
│   Browser sends POST to bank.com WITH session cookie attached    │
│        │                                                          │
│        ▼                                                          │
│   bank.com sees valid session → executes transfer                │
│                                                                   │
│   ⚠️  Server cannot tell this request came from evil.com          │
└─────────────────────────────────────────────────────────────────┘
AttackWhat it exploitsDirectionCredential handling
CSRFServer’s trust in the browser’s cookiesCross-site → target serverBrowser auto-attaches credentials
XSSClient’s trust in server contentInjected script runs in target originFull access to cookies, DOM, storage
ClickjackingUser’s visual perceptionOverlaid frames trick clicksUser actively clicks (visible action)
SSRFServer’s ability to make requestsServer → internal resourcesServer’s own credentials
CORS misconfigurationOverly permissive access controlCross-origin readsMay allow credential-bearing reads

Key distinction: CSRF is a write-focused attack (cause side effects). XSS is a read+write attack (full origin access). CORS protects reading cross-origin responses, not sending requests.

Browser Trust Model

┌─────────────────────────────────────────────────────────────┐
│                Browser Security Model                         │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  Same-Origin Policy (SOP):                                   │
│    - Prevents READING cross-origin responses                 │
│    - Does NOT prevent SENDING cross-origin requests          │
│    - Origin = scheme + host + port                           │
│                                                               │
│  Cookie Policy:                                               │
│    - Cookies attached based on DESTINATION domain            │
│    - Not based on which page initiated the request           │
│    - SameSite attribute modifies this behavior               │
│                                                               │
│  Request Initiation:                                         │
│    - Any page can submit forms to any URL                    │
│    - Any page can load images from any URL                   │
│    - Any page can trigger navigation to any URL              │
│    - JavaScript fetch/XHR: limited by CORS for reads         │
│      but requests ARE still sent (simple requests)           │
│                                                               │
└─────────────────────────────────────────────────────────────┘

Origin vs Site

ConceptDefinitionExample
Originscheme + host + porthttps://app.example.com:443
Sitescheme + eTLD+1https://example.com
Same-originExact scheme + host + port matchhttps://a.example.comhttps://b.example.com
Same-siteSame scheme + eTLD+1https://a.example.com = https://b.example.com

This distinction matters because SameSite cookies use the site definition, not origin. A subdomain can still send same-site requests.

CSRF Attack Lifecycle

1. VICTIM AUTHENTICATES
   User logs into bank.com → receives session cookie
   
2. ATTACKER SITE LOADED
   User visits evil.com (phishing link, ad, forum post)
   
3. FORGED REQUEST CREATED
   evil.com contains hidden form/script targeting bank.com
   
4. BROWSER AUTO-ATTACHES CREDENTIALS
   Browser sees request to bank.com → attaches bank.com cookies
   
5. SERVER TRUSTS REQUEST
   bank.com receives valid session → processes request
   
6. UNAUTHORIZED ACTION SUCCEEDS
   Money transferred, email changed, password reset, etc.

CSRF Variants Comparison

VariantMechanismSeverityCommon Target
Traditional CSRFHidden form auto-submitsHighState-changing endpoints
Login CSRFForces login with attacker’s accountMedium-HighOAuth/login endpoints
Stored CSRFCSRF payload stored in target site (via HTML injection)CriticalForums, user content
API CSRFFetch/XHR with credentialsHighJSON APIs using cookies
Client-side CSRFManipulates client-side routing/stateMediumSPAs with client-side logic

When CSRF Becomes Critical

Real-World Impact


2. Browser Networking & Security Deep Dive

How Browsers Attach Cookies

When the browser makes ANY request to example.com, it checks its cookie jar:

Request to: https://example.com/api/transfer
Cookie jar lookup: domain=example.com, path=/, secure=true

Result: Cookie: session=abc123; csrf_token=xyz789

Attached regardless of:
  - Which tab/window initiated the request
  - Which origin the request came from
  - Whether it was user-initiated or script-initiated
  - Whether it was a form submission, image load, or fetch()
  
UNLESS SameSite restrictions apply.

SameSite is the most important modern CSRF defense at the browser level.

ValueCross-site GETCross-site POSTCross-site fetchTop-level navigation
Strict❌ Blocked❌ Blocked❌ Blocked❌ Blocked
Lax❌ Blocked❌ Blocked❌ Blocked✅ Sent (top-level GET only)
None✅ Sent✅ Sent✅ Sent✅ Sent
Not set (modern default)Same as LaxSame as LaxSame as LaxSame as Lax

Critical nuance of Lax:

Why Lax allows top-level GET navigations: If Strict were default, clicking a link to GitHub from Google would log you out — terrible UX. Lax is the compromise between security and usability.

The “Lax+POST” 2-minute window (Chrome): Chrome previously had a temporary exception: cookies without SameSite set were treated as Lax but still sent on top-level cross-site POST within 2 minutes of being set. This was removed in later versions.

Fetch Credentials Modes

// credentials: "omit" — never send cookies
fetch("https://api.example.com/data", { credentials: "omit" });

// credentials: "same-origin" — only send for same-origin (DEFAULT)
fetch("https://api.example.com/data", { credentials: "same-origin" });

// credentials: "include" — always send cookies (even cross-origin)
fetch("https://api.example.com/data", { credentials: "include" });

Critical insight: Even with credentials: "include", SameSite restrictions still apply at the browser level. The credentials option tells the browser you WANT to send cookies, but SameSite may still prevent it.

Why Forms Are Dangerous

Forms are the original CSRF vector because:

  1. No CORS preflight — Form submissions are “simple requests”
  2. No JavaScript needed — Pure HTML can submit forms
  3. Auto-submit possible<body onload="document.forms[0].submit()">
  4. No SOP restriction on sending — SOP only restricts reading responses
  5. Cookies auto-attached — Based on destination domain
  6. Content-Type allowedapplication/x-www-form-urlencoded, multipart/form-data, text/plain
<!-- Attacker page: auto-submitting hidden form -->
<body onload="document.getElementById('csrf-form').submit()">
  <form id="csrf-form" action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="attacker-account" />
    <input type="hidden" name="amount" value="10000" />
  </form>
</body>

Why GET Requests Can Be Abused

If a server performs state-changing operations on GET:

<!-- Image tag triggers GET with cookies -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />

<!-- Link click (top-level navigation, bypasses SameSite=Lax) -->
<a href="https://bank.com/transfer?to=attacker&amount=10000">Click here</a>

Rule: NEVER perform state-changing operations on GET endpoints.

┌─────────────────────────────────────────────────────┐
│              Request Classification                    │
├─────────────────────────────────────────────────────┤
│                                                       │
│  TOP-LEVEL NAVIGATION:                               │
│    - Clicking links                                   │
│    - Form submissions                                │
│    - window.location = "..."                         │
│    - <meta http-equiv="refresh">                     │
│    → SameSite=Lax cookies SENT (GET only)           │
│                                                       │
│  SUBRESOURCE REQUESTS:                               │
│    - <img src="...">                                 │
│    - <script src="...">                              │
│    - <iframe src="...">                              │
│    - fetch() / XMLHttpRequest                        │
│    - CSS @import                                     │
│    → SameSite=Lax cookies BLOCKED                   │
│                                                       │
└─────────────────────────────────────────────────────┘

Preflight Requests and CSRF

CORS preflight (OPTIONS) is triggered when:

Why custom headers help prevent CSRF:

Simple request (NO preflight):         Preflighted request:
  POST + form-urlencoded                 POST + application/json
  → Request IS sent                      → OPTIONS sent first
  → Response may be blocked              → If denied, request NOT sent
  → Side effect ALREADY happened         → No side effect

Browser Engine Differences

BehaviorChromium (Blink)Firefox (Gecko)Safari (WebKit)
Default SameSiteLaxLaxLax (since 2021)
Lax+POST windowRemovedNever hadNever had
Schemeful same-siteYes (http≠https)YesYes
Partition cookies (CHIPS)SupportedSupportedITP (different approach)
3P cookie blockingPhase-out pausedETP strictITP full block

3. CSRF Attack Types Deep Dive

3.1 Form-Based CSRF

The classic attack. An attacker creates a hidden form that auto-submits to the target.

<!-- Hosted on evil.com -->
<html>
<body onload="document.getElementById('f').submit()">
  <form id="f" method="POST" action="https://bank.com/api/transfer">
    <input type="hidden" name="recipient" value="attacker-acct" />
    <input type="hidden" name="amount" value="50000" />
    <input type="hidden" name="currency" value="USD" />
  </form>
</body>
</html>

Why it works:

Mitigation: CSRF tokens, SameSite=Lax/Strict, Origin validation

3.2 Image-Based CSRF (GET)

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

Only works if: Server performs state changes on GET (a vulnerability itself).

3.3 Login CSRF

Forces victim to log in with attacker’s credentials:

<form action="https://target.com/login" method="POST">
  <input type="hidden" name="email" value="attacker@evil.com" />
  <input type="hidden" name="password" value="attacker-password" />
</form>
<script>document.forms[0].submit();</script>

Why it’s dangerous:

3.4 JSON CSRF

Targeting APIs that accept JSON:

<!-- Method 1: Using text/plain content type (no preflight) -->
<form action="https://api.target.com/transfer" method="POST" 
      enctype="text/plain">
  <input name='{"to":"attacker","amount":"10000","ignore":"' value='"}' />
</form>

This sends: {"to":"attacker","amount":"10000","ignore":"="} as text/plain.

Works when:

Doesn’t work when:

3.5 GraphQL CSRF

<!-- GraphQL often accepts GET with query parameter -->
<img src='https://api.target.com/graphql?query=mutation{transferFunds(to:"attacker",amount:10000){id}}' />

<!-- Or POST with application/x-www-form-urlencoded -->
<form action="https://api.target.com/graphql" method="POST">
  <input name="query" value='mutation { deleteAccount(id: "victim") { success } }' />
</form>

GraphQL-specific risks:

3.6 OAuth CSRF

Attack flow:
1. Attacker initiates OAuth login → gets authorization code
2. Attacker does NOT complete the flow (doesn't exchange code)
3. Attacker tricks victim into visiting callback URL:
   https://target.com/auth/callback?code=ATTACKER_CODE
4. Target exchanges code → links attacker's external account to victim
5. Attacker now has access via OAuth provider

Mitigation: OAuth state parameter (cryptographic nonce bound to user session), PKCE.

3.7 iframe-Based CSRF

<!-- Hidden iframe loads target page, then submits form -->
<iframe name="csrf-frame" style="display:none"></iframe>
<form action="https://target.com/settings" method="POST" target="csrf-frame">
  <input type="hidden" name="email" value="attacker@evil.com" />
</form>
<script>document.forms[0].submit();</script>

Why iframe variant is stealthy:

3.8 Client-Side CSRF

When client-side JavaScript reads attacker-controlled input and uses it to make requests:

// Vulnerable: reads URL parameter and uses it as API endpoint
const endpoint = new URLSearchParams(location.search).get("api");
fetch(endpoint, { method: "POST", credentials: "include", body: data });

Attack: https://target.com/app?api=https://target.com/admin/delete-user

3.9 Mobile WebView CSRF


4. CSRF Defense Mechanisms Deep Dive

4.1 Synchronizer Token Pattern

The gold standard for server-rendered applications.

┌──────────────────────────────────────────────────────────┐
│           Synchronizer Token Pattern                       │
├──────────────────────────────────────────────────────────┤
│                                                            │
│  1. Server generates random token per session              │
│  2. Token stored in server-side session                    │
│  3. Token embedded in form as hidden field                 │
│  4. On submission, server compares form token to session   │
│  5. Mismatch → reject request                             │
│                                                            │
│  WHY IT WORKS:                                            │
│  - Attacker cannot READ the token (SOP blocks response)   │
│  - Attacker cannot GUESS the token (cryptographic random) │
│  - Token proves request originated from legitimate page    │
│                                                            │
└──────────────────────────────────────────────────────────┘
// Express middleware example
import crypto from "crypto";

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

// Validate token
function validateCsrfToken(req: Request, session: Session): boolean {
  const token = req.body._csrf || req.headers["x-csrf-token"];
  return token && crypto.timingSafeEqual(
    Buffer.from(token),
    Buffer.from(session.csrfToken)
  );
}

Stateless alternative — no server-side storage needed.

┌──────────────────────────────────────────────────────────┐
│           Double-Submit Cookie Pattern                      │
├──────────────────────────────────────────────────────────┤
│                                                            │
│  1. Server sets CSRF token as a cookie                     │
│  2. Client reads cookie value via JavaScript               │
│  3. Client includes token in request header/body           │
│  4. Server compares cookie value to header/body value      │
│  5. Mismatch → reject                                     │
│                                                            │
│  WHY IT WORKS:                                            │
│  - Attacker can cause browser to SEND the cookie           │
│  - But attacker cannot READ the cookie (SOP)              │
│  - So attacker cannot include matching value in body       │
│                                                            │
│  WEAKNESS:                                                │
│  - Subdomain attacks: subdomain can set parent cookies    │
│  - Must use __Host- prefix to prevent this                │
│                                                            │
└──────────────────────────────────────────────────────────┘
// Server sets cookie
res.cookie("__Host-csrf", token, {
  httpOnly: false, // Must be readable by JS
  secure: true,
  sameSite: "Lax",
  path: "/",
});

// Client reads and sends
const csrfToken = document.cookie
  .split("; ")
  .find(row => row.startsWith("__Host-csrf="))
  ?.split("=")[1];

fetch("/api/action", {
  method: "POST",
  headers: { "X-CSRF-Token": csrfToken },
  credentials: "same-origin",
});

4.3 SameSite Cookies

Browser-level defense — the most impactful modern protection.

// Setting secure session cookie
res.cookie("session", sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: "Lax",   // or "Strict" for maximum protection
  path: "/",
  maxAge: 3600000,
  domain: undefined, // Don't set domain — more restrictive
});
StrategyUse CaseTrade-off
StrictBanking, admin panelsBreaks inbound links (user appears logged out)
LaxMost applicationsProtects POST but allows GET navigation
Lax + CSRF tokenHigh-security with good UXBelt-and-suspenders approach
Dual cookiesStrict for writes, Lax for readsComplex but optimal UX+security

Dual cookie pattern:

// Strict cookie for mutation authentication
res.cookie("__Host-session-strict", token, { sameSite: "Strict", ... });
// Lax cookie for read-only authentication  
res.cookie("__Host-session-lax", token, { sameSite: "Lax", ... });

// Server logic:
// GET requests: accept Lax cookie (user stays logged in via links)
// POST/PUT/DELETE: require Strict cookie (blocks cross-site)

4.4 Origin Header Validation

function validateOrigin(req: Request): boolean {
  const origin = req.headers["origin"];
  const referer = req.headers["referer"];
  
  // Origin is most reliable (sent on POST, PUT, DELETE, PATCH)
  if (origin) {
    return ALLOWED_ORIGINS.includes(origin);
  }
  
  // Fallback to Referer (may be stripped by Referrer-Policy)
  if (referer) {
    const refererOrigin = new URL(referer).origin;
    return ALLOWED_ORIGINS.includes(refererOrigin);
  }
  
  // No Origin AND no Referer — reject (defense-in-depth)
  // Some legitimate cases: bookmarks, typed URLs
  // Handle based on your risk tolerance
  return false;
}

Why Origin validation helps:

Why it’s imperfect:

4.5 Custom Request Headers

// Client: Always send custom header
fetch("/api/action", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest", // triggers preflight
  },
  credentials: "same-origin",
});

Why custom headers prevent CSRF:

  1. Adding ANY custom header triggers CORS preflight
  2. Preflight sends OPTIONS request to server
  3. If server doesn’t respond with Access-Control-Allow-Headers: X-Requested-With
  4. Browser blocks the actual request entirely
  5. The request with side effects is never sent

Important: This only works for XHR/fetch requests. Forms cannot set custom headers.

4.6 Defense Comparison

DefenseStateless?Browser supportSubdomain-safe?Effort
Synchronizer tokenNoUniversalYesMedium
Double-submit cookieYesUniversalNeed __Host- prefixMedium
SameSite=LaxYesModernYes (scheme-aware)Low
SameSite=StrictYesModernYesLow (UX cost)
Origin validationYesModernYesLow
Custom headersYesUniversalYesLow
CORS properly configuredModernYesMedium

4.7 Defense-in-Depth Strategy

Recommended layered approach:

Layer 1: SameSite=Lax on all cookies (browser enforcement)
Layer 2: Origin/Referer validation middleware
Layer 3: CSRF tokens for state-changing operations
Layer 4: Custom headers (Content-Type: application/json)
Layer 5: Strict CORS configuration
Layer 6: Re-authentication for critical actions

Why JWT Apps Are NOT Automatically Safe

Common misconception: “We use JWTs, so we’re immune to CSRF.”

Reality:

JWT Storage      | CSRF Risk | XSS Risk | Recommendation
─────────────────┼───────────┼──────────┼──────────────────
localStorage     | None      | HIGH     | Avoid for auth
httpOnly cookie  | HIGH      | Low      | Add CSRF protection
Memory (var)     | None      | Medium   | Short-lived only
httpOnly + header| None      | Low      | Best (complex setup)

5. Cookies, Sessions & Authentication Architecture

// Maximum security cookie configuration
res.cookie("__Host-session", value, {
  httpOnly: true,     // Not accessible to JavaScript
  secure: true,       // HTTPS only
  sameSite: "Lax",   // Cross-site protection
  path: "/",         // Available to entire site
  maxAge: 3600000,   // 1 hour
  // domain: NOT SET  // __Host- prefix requires no domain
});
AttributePurposeCSRF Impact
HttpOnlyBlocks JS accessPrevents XSS from reading token (indirect CSRF help)
SecureHTTPS onlyPrevents MITM cookie theft
SameSiteCross-site controlPrimary browser-level CSRF defense
__Host- prefixRequires Secure + no Domain + path=/Prevents subdomain cookie injection
__Secure- prefixRequires Secure flagMinimum transport security
┌──────────────────────────────────────────────────────────┐
│  Session Cookie Auth          │  JWT Cookie Auth          │
├───────────────────────────────┼───────────────────────────┤
│  Cookie: session=abc123       │  Cookie: token=eyJhbG...  │
│  Server looks up session      │  Server verifies JWT sig  │
│  Server has revocation        │  No built-in revocation   │
│  Stateful (session store)     │  Stateless                │
│  CSRF: vulnerable             │  CSRF: vulnerable         │
│  Both need SameSite/tokens    │  Both need SameSite/tokens│
└───────────────────────────────┴───────────────────────────┘

KEY INSIGHT: The storage mechanism (cookie) creates CSRF risk,
not the token format. Both session IDs and JWTs in cookies are 
equally vulnerable to CSRF.

Authentication Architecture Comparison

ArchitectureCSRF RiskXSS RiskComplexityBest For
Session cookie (httpOnly, SameSite=Lax)Low (with SameSite)LowLowTraditional apps, SSR
JWT in httpOnly cookieLow (with SameSite)LowMediumStateless APIs
JWT in localStorage + Auth headerNoneHIGHLowAvoid
JWT in memory + refresh cookieNoneMediumHighSPAs needing max security
BFF pattern (Backend-for-Frontend)LowLowHighEnterprise SPAs
┌─────────────────────────────────────────────────────────┐
│                    BFF Pattern                            │
├─────────────────────────────────────────────────────────┤
│                                                           │
│  Browser                                                 │
│    │  (httpOnly session cookie, SameSite=Strict)        │
│    ▼                                                     │
│  BFF (same-origin Node.js server)                       │
│    │  (Authorization: Bearer <token>)                   │
│    ▼                                                     │
│  API Server                                              │
│                                                           │
│  Benefits:                                               │
│  - No tokens exposed to JavaScript                      │
│  - Session cookie is httpOnly + SameSite                │
│  - BFF handles token refresh                            │
│  - CSRF protection via SameSite + tokens                │
│  - API server only accepts bearer tokens (no cookies)   │
│                                                           │
└─────────────────────────────────────────────────────────┘

Refresh Token Architecture

// Secure refresh pattern
// 1. Access token: short-lived, in memory (not cookie)
// 2. Refresh token: long-lived, httpOnly cookie, SameSite=Strict

// Login response
res.cookie("__Host-refresh", refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: "Strict",
  path: "/api/auth/refresh", // Only sent to refresh endpoint
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});

// Client stores access token in memory
let accessToken = response.data.accessToken; // 15-min expiry

// Refresh endpoint
app.post("/api/auth/refresh", csrfProtection, (req, res) => {
  const refreshToken = req.cookies["__Host-refresh"];
  // Validate, rotate, issue new tokens
});

Session Fixation Prevention

// After successful authentication, ALWAYS regenerate session
app.post("/login", async (req, res) => {
  const user = await authenticate(req.body);
  
  // Destroy old session and create new one
  req.session.regenerate((err) => {
    req.session.userId = user.id;
    req.session.csrfToken = crypto.randomBytes(32).toString("hex");
    res.redirect("/dashboard");
  });
});

6. React / Next.js / Astro CSRF Deep Dive

React SPA CSRF Patterns

Problem: React SPAs typically use fetch() to call APIs. If authentication uses cookies, CSRF is still a risk.

// ❌ VULNERABLE: Cookie-based auth without CSRF protection
fetch("/api/transfer", {
  method: "POST",
  credentials: "same-origin", // sends cookies
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ to: "recipient", amount: 1000 }),
});

// ✅ SAFE: With CSRF token in header
function useCsrfFetch() {
  const csrfToken = useCsrfToken(); // Read from meta tag or cookie
  
  return (url: string, options: RequestInit = {}) => {
    return fetch(url, {
      ...options,
      credentials: "same-origin",
      headers: {
        ...options.headers,
        "Content-Type": "application/json",
        "X-CSRF-Token": csrfToken,
      },
    });
  };
}

CSRF Token Delivery for SPAs:

// Option 1: Meta tag (set by server on page load)
// In SSR HTML: <meta name="csrf-token" content="{{token}}" />
const token = document.querySelector('meta[name="csrf-token"]')?.content;

// Option 2: Cookie (double-submit pattern)
// Server sets: Set-Cookie: __Host-csrf=token123; Path=/; Secure
const token = getCookie("__Host-csrf");

// Option 3: Dedicated endpoint (for pure SPAs)
const { data } = await fetch("/api/auth/csrf-token", { credentials: "same-origin" });

Next.js CSRF Protection

App Router (Server Actions)

// Next.js Server Actions have built-in CSRF protection
// They require the Origin header to match, and use POST + custom headers

// app/actions.ts
"use server";

import { cookies } from "next/headers";

export async function transferFunds(formData: FormData) {
  // Server Actions automatically validate:
  // 1. Origin header matches
  // 2. Request uses POST
  // 3. Content-Type is multipart/form-data or application/x-www-form-urlencoded
  
  const session = cookies().get("session");
  // ... process action
}

API Routes Protection

// app/api/transfer/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  // Validate Origin
  const origin = req.headers.get("origin");
  if (origin !== process.env.ALLOWED_ORIGIN) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }
  
  // Validate CSRF token (double-submit)
  const cookieToken = req.cookies.get("__Host-csrf")?.value;
  const headerToken = req.headers.get("x-csrf-token");
  
  if (!cookieToken || !headerToken || cookieToken !== headerToken) {
    return NextResponse.json({ error: "Invalid CSRF token" }, { status: 403 });
  }
  
  // Process request...
}

Middleware CSRF Validation

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const SAFE_METHODS = ["GET", "HEAD", "OPTIONS"];

export function middleware(req: NextRequest) {
  if (SAFE_METHODS.includes(req.method)) {
    return NextResponse.next();
  }
  
  // Validate Origin for all state-changing requests
  const origin = req.headers.get("origin");
  const host = req.headers.get("host");
  
  if (!origin || new URL(origin).host !== host) {
    return NextResponse.json(
      { error: "CSRF validation failed" },
      { status: 403 }
    );
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: "/api/:path*",
};

Astro CSRF Protection

// src/pages/api/action.ts (Astro API route)
import type { APIRoute } from "astro";

export const POST: APIRoute = async ({ request, cookies }) => {
  // Validate Origin
  const origin = request.headers.get("origin");
  const url = new URL(request.url);
  
  if (origin !== url.origin) {
    return new Response("CSRF rejected", { status: 403 });
  }
  
  // Double-submit validation
  const csrfCookie = cookies.get("__Host-csrf")?.value;
  const csrfHeader = request.headers.get("x-csrf-token");
  
  if (!csrfCookie || csrfCookie !== csrfHeader) {
    return new Response("Invalid CSRF token", { status: 403 });
  }
  
  // Process...
  return new Response(JSON.stringify({ success: true }), { status: 200 });
};
---
// src/pages/form.astro — Server-rendered form with CSRF token
import crypto from "crypto";

const csrfToken = crypto.randomBytes(32).toString("hex");
Astro.cookies.set("__Host-csrf", csrfToken, {
  httpOnly: false,
  secure: true,
  sameSite: "strict",
  path: "/",
});
---

<form method="POST" action="/api/action">
  <input type="hidden" name="_csrf" value={csrfToken} />
  <button type="submit">Submit</button>
</form>

Why SPAs Still Face CSRF

Misconception: “SPAs use JSON APIs with fetch, so CSRF isn’t possible.”

Reality:

  1. If using credentials: "include" or "same-origin" → cookies are sent
  2. If API accepts Content-Type: text/plain → no preflight (exploit possible)
  3. If API accepts form-encoded data → classic form CSRF works
  4. If GET endpoints have side effects → image/link-based CSRF works
  5. Even with JSON + custom headers → ensure CORS is strict

SPA is safe from CSRF only when ALL of:

fetch vs axios Defaults

Behaviorfetch()axios
Credentialssame-origin (default)No cookies by default
Cross-origin cookiesMust set credentials: "include"Must set withCredentials: true
Content-TypeNone (unless set)application/json (auto)
Preflight triggerMust add custom header manuallyAuto (due to Content-Type: application/json)
// axios global config for CSRF
import axios from "axios";

const api = axios.create({
  baseURL: "/api",
  withCredentials: true,
  headers: {
    "X-Requested-With": "XMLHttpRequest",
  },
});

// Read CSRF token from cookie and attach to every request
api.interceptors.request.use((config) => {
  const token = getCookie("__Host-csrf");
  if (token) {
    config.headers["X-CSRF-Token"] = token;
  }
  return config;
});

7. CORS, SOP & CSRF Relationship

Why CORS Does NOT Stop CSRF

┌──────────────────────────────────────────────────────────────┐
│         Common Misconception vs Reality                        │
├──────────────────────────────────────────────────────────────┤
│                                                                │
│  MISCONCEPTION:                                               │
│  "CORS blocks cross-origin requests, so CSRF is prevented"   │
│                                                                │
│  REALITY:                                                     │
│  CORS controls whether the RESPONSE is readable              │
│  The REQUEST is still SENT (for simple requests)             │
│  Side effects happen BEFORE the response is checked          │
│                                                                │
│  ┌─────────┐    POST /transfer     ┌──────────┐             │
│  │ evil.com │ ──────────────────► │ bank.com  │             │
│  │          │    (cookies sent!)   │           │             │
│  │          │ ◄────────────────── │ (executed!)│             │
│  │          │    Response blocked  │           │             │
│  └─────────┘    by CORS           └──────────┘             │
│                                                                │
│  The transfer ALREADY HAPPENED.                              │
│  CORS just prevented evil.com from reading the response.     │
│                                                                │
└──────────────────────────────────────────────────────────────┘

CORS DOES help when:

Same-Origin vs Same-Site

https://app.example.com:443/path

Origin: https://app.example.com:443
Site:   https://example.com (eTLD+1)

┌────────────────────────────────────────────────────────────┐
│  URL A                      │ URL B                │ Same? │
├─────────────────────────────┼──────────────────────┼───────┤
│ https://a.example.com       │ https://b.example.com│       │
│   Same-origin?              │                      │  NO   │
│   Same-site?                │                      │  YES  │
├─────────────────────────────┼──────────────────────┼───────┤
│ https://example.com         │ http://example.com   │       │
│   Same-origin?              │                      │  NO   │
│   Same-site?                │                      │  NO*  │
│   * Schemeful same-site     │                      │       │
├─────────────────────────────┼──────────────────────┼───────┤
│ https://example.com:443     │ https://example.com  │       │
│   Same-origin?              │                      │  YES  │
│   Same-site?                │                      │  YES  │
└─────────────────────────────┴──────────────────────┴───────┘

Why Custom Headers Help

Standard form submission (NO preflight):
  POST /api/transfer HTTP/1.1
  Content-Type: application/x-www-form-urlencoded
  Cookie: session=abc123
  
  → Request SENT → Side effect happens → CORS blocks response (too late)

Request with custom header (PREFLIGHT required):
  OPTIONS /api/transfer HTTP/1.1
  Access-Control-Request-Headers: X-CSRF-Token
  
  Server responds: "I don't allow X-CSRF-Token from your origin"
  → Actual request NEVER SENT → No side effect

8. Secure Architecture & Enterprise CSRF Strategy

Enterprise CSRF Defense Architecture

┌──────────────────────────────────────────────────────────────┐
│              Enterprise Defense Layers                         │
├──────────────────────────────────────────────────────────────┤
│                                                                │
│  CDN/Edge Layer                                               │
│    └─ Rate limiting, bot detection                           │
│                                                                │
│  API Gateway                                                  │
│    └─ Origin validation                                      │
│    └─ CORS enforcement                                       │
│    └─ Request classification                                 │
│                                                                │
│  Application Layer                                            │
│    └─ CSRF token validation                                  │
│    └─ Session management                                     │
│    └─ Re-authentication for critical ops                     │
│                                                                │
│  Cookie Policy                                                │
│    └─ SameSite=Lax (default)                                │
│    └─ SameSite=Strict (admin/financial)                      │
│    └─ __Host- prefix (all session cookies)                   │
│    └─ Secure + HttpOnly                                      │
│                                                                │
│  Monitoring                                                   │
│    └─ CSRF failure alerting                                  │
│    └─ Origin anomaly detection                               │
│    └─ Session usage patterns                                 │
│                                                                │
└──────────────────────────────────────────────────────────────┘

CSRF Review Checklist

## Pre-deployment CSRF Checklist

### Cookies
- [ ] All session cookies have SameSite=Lax minimum
- [ ] Admin/sensitive cookies use SameSite=Strict
- [ ] All cookies use Secure flag
- [ ] Session cookies use HttpOnly
- [ ] Using __Host- prefix for critical cookies
- [ ] No overly broad Domain attribute

### Endpoints
- [ ] All state-changing operations use POST/PUT/DELETE (never GET)
- [ ] Origin header validated on state-changing requests
- [ ] CSRF tokens required for form submissions
- [ ] APIs require Content-Type: application/json (triggers preflight)
- [ ] Custom header required (X-Requested-With or similar)

### CORS
- [ ] Access-Control-Allow-Origin is NOT wildcard with credentials
- [ ] Allowed origins are explicitly listed
- [ ] Access-Control-Allow-Credentials only when needed
- [ ] No origin reflection (don't echo back Origin header)

### Authentication
- [ ] Session regeneration after login
- [ ] Re-authentication for critical actions (password change, etc.)
- [ ] Token rotation on privilege changes
- [ ] Proper logout (server-side session destruction)

### Framework
- [ ] CSRF middleware enabled and configured
- [ ] Token validation on all non-GET routes
- [ ] SPA receives CSRF token securely
- [ ] GraphQL mutations protected

Multi-Tenant SaaS Architecture

// Multi-tenant CSRF considerations
// Each tenant may have custom domains: tenant1.app.com, tenant2.app.com

// Problem: SameSite considers all subdomains as same-site
// Solution: Use __Host- prefix (no Domain attribute = exact host match)

// Middleware for multi-tenant CSRF
function multiTenantCsrf(req: Request, res: Response, next: NextFunction) {
  const tenantId = extractTenantFromHost(req.headers.host);
  const sessionTenant = req.session.tenantId;
  
  // Ensure request targets same tenant as session
  if (tenantId !== sessionTenant) {
    return res.status(403).json({ error: "Cross-tenant request rejected" });
  }
  
  // Standard CSRF validation
  validateCsrfToken(req, res, next);
}

Microfrontend CSRF Strategy

┌──────────────────────────────────────────────────────────┐
│           Microfrontend CSRF Architecture                  │
├──────────────────────────────────────────────────────────┤
│                                                            │
│  Shell App (app.example.com)                              │
│    ├─ MFE-A (app.example.com/feature-a)                  │
│    ├─ MFE-B (app.example.com/feature-b)                  │
│    └─ Shared Auth Module                                  │
│                                                            │
│  Strategy:                                                │
│  1. Single session cookie at shell level                  │
│  2. Shared CSRF token service                            │
│  3. Each MFE reads token from shared cookie              │
│  4. Central API gateway validates                        │
│                                                            │
│  If different origins (mfe-a.example.com):               │
│  - BFF pattern per MFE                                   │
│  - Or shared auth service with token exchange            │
│                                                            │
└──────────────────────────────────────────────────────────┘

9. OAuth / OIDC / SSO CSRF Deep Dive

OAuth State Parameter

The state parameter is OAuth’s CSRF protection:

┌──────────────────────────────────────────────────────────┐
│           OAuth CSRF Attack (without state)                │
├──────────────────────────────────────────────────────────┤
│                                                            │
│  1. Attacker starts OAuth flow → gets code                │
│  2. Attacker captures callback URL:                       │
│     https://app.com/callback?code=ATTACKER_CODE           │
│  3. Tricks victim into visiting that URL                  │
│  4. App exchanges code → gets attacker's tokens           │
│  5. App links attacker's account to victim's session      │
│  6. Attacker now accesses victim's data via linked acct   │
│                                                            │
├──────────────────────────────────────────────────────────┤
│           OAuth with state (CSRF protected)               │
│                                                            │
│  1. App generates random state, stores in session         │
│  2. Auth URL includes: &state=random123                  │
│  3. Provider redirects back: ?code=xxx&state=random123   │
│  4. App verifies state matches session                    │
│  5. Attacker's callback has different state → rejected    │
│                                                            │
└──────────────────────────────────────────────────────────┘
// Secure OAuth implementation
import crypto from "crypto";

// Step 1: Generate and store state
app.get("/auth/login", (req, res) => {
  const state = crypto.randomBytes(32).toString("hex");
  const codeVerifier = crypto.randomBytes(32).toString("base64url"); // PKCE
  const codeChallenge = crypto
    .createHash("sha256")
    .update(codeVerifier)
    .digest("base64url");
  
  req.session.oauthState = state;
  req.session.codeVerifier = codeVerifier;
  
  const authUrl = new URL("https://provider.com/authorize");
  authUrl.searchParams.set("client_id", CLIENT_ID);
  authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
  authUrl.searchParams.set("state", state);
  authUrl.searchParams.set("code_challenge", codeChallenge);
  authUrl.searchParams.set("code_challenge_method", "S256");
  authUrl.searchParams.set("response_type", "code");
  
  res.redirect(authUrl.toString());
});

// Step 2: Validate state on callback
app.get("/auth/callback", (req, res) => {
  const { code, state } = req.query;
  
  // CSRF check: validate state
  if (!state || state !== req.session.oauthState) {
    return res.status(403).send("OAuth state mismatch — possible CSRF");
  }
  
  // Clear state (one-time use)
  delete req.session.oauthState;
  
  // Exchange code with PKCE verifier
  const tokens = await exchangeCode(code, req.session.codeVerifier);
  delete req.session.codeVerifier;
  
  // Continue authentication...
});

Login CSRF

Often overlooked but dangerous:

Attack scenario:
1. Attacker crafts form that logs victim into attacker's account
2. Victim unknowingly operates as attacker
3. Victim adds payment method, enters sensitive data
4. Attacker logs back in → accesses victim's data

Defense:
- CSRF protection on login endpoint
- Session regeneration after login
- Display account identity prominently
- SameSite=Lax blocks cross-site POST (login forms)

SPA OAuth Strategy

// Recommended: Authorization Code + PKCE (no client secret)
// For SPAs that can't securely store client_secret

// 1. Generate PKCE values client-side
function generatePKCE() {
  const verifier = crypto.randomUUID() + crypto.randomUUID();
  const challenge = await sha256Base64Url(verifier);
  
  // Store verifier in sessionStorage (survives redirect)
  sessionStorage.setItem("pkce_verifier", verifier);
  
  // Generate state for CSRF
  const state = crypto.randomUUID();
  sessionStorage.setItem("oauth_state", state);
  
  return { challenge, state };
}

// 2. On callback, validate state
function handleCallback() {
  const params = new URLSearchParams(location.search);
  const state = params.get("state");
  const storedState = sessionStorage.getItem("oauth_state");
  
  if (state !== storedState) {
    throw new Error("CSRF detected: state mismatch");
  }
  
  sessionStorage.removeItem("oauth_state");
  // Exchange code...
}

10. Setup Guide

Complete CSRF Protection Setup

// Express/NestJS session setup
import session from "express-session";

app.use(session({
  name: "__Host-session",
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: true, // production: always true
    sameSite: "lax",
    maxAge: 60 * 60 * 1000, // 1 hour
    path: "/",
  },
}));

Step 2: CSRF Middleware (Express)

// csrf-middleware.ts
import crypto from "crypto";
import { Request, Response, NextFunction } from "express";

const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);

export function csrfProtection(req: Request, res: Response, next: NextFunction) {
  // Skip safe methods
  if (SAFE_METHODS.has(req.method)) {
    return next();
  }
  
  // Layer 1: Origin validation
  const origin = req.headers.origin;
  const expectedOrigin = `${req.protocol}://${req.headers.host}`;
  
  if (origin && origin !== expectedOrigin) {
    return res.status(403).json({ error: "Origin mismatch" });
  }
  
  // Layer 2: CSRF token validation (double-submit)
  const cookieToken = req.cookies["__Host-csrf"];
  const headerToken = req.headers["x-csrf-token"] as string;
  
  if (!cookieToken || !headerToken) {
    return res.status(403).json({ error: "Missing CSRF token" });
  }
  
  if (!crypto.timingSafeEqual(
    Buffer.from(cookieToken),
    Buffer.from(headerToken)
  )) {
    return res.status(403).json({ error: "CSRF token mismatch" });
  }
  
  next();
}

// Token generation endpoint
export function csrfTokenEndpoint(req: Request, res: Response) {
  const token = crypto.randomBytes(32).toString("hex");
  
  res.cookie("__Host-csrf", token, {
    httpOnly: false, // JS must read it
    secure: true,
    sameSite: "strict",
    path: "/",
    maxAge: 60 * 60 * 1000,
  });
  
  res.json({ csrfToken: token });
}

Step 3: NestJS Guard

// csrf.guard.ts
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from "@nestjs/common";
import crypto from "crypto";

@Injectable()
export class CsrfGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();
    
    if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
      return true;
    }
    
    const origin = req.headers.origin;
    const host = req.headers.host;
    
    if (origin && new URL(origin).host !== host) {
      throw new ForbiddenException("CSRF: Origin mismatch");
    }
    
    const cookieToken = req.cookies?.["__Host-csrf"];
    const headerToken = req.headers["x-csrf-token"];
    
    if (!cookieToken || !headerToken) {
      throw new ForbiddenException("CSRF: Missing token");
    }
    
    try {
      if (!crypto.timingSafeEqual(Buffer.from(cookieToken), Buffer.from(headerToken))) {
        throw new ForbiddenException("CSRF: Token mismatch");
      }
    } catch {
      throw new ForbiddenException("CSRF: Validation failed");
    }
    
    return true;
  }
}

Step 4: React API Client Setup

// api-client.ts
import axios from "axios";

const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL || "/api",
  withCredentials: true,
  headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest",
  },
});

// Attach CSRF token from cookie
api.interceptors.request.use((config) => {
  if (!["get", "head", "options"].includes(config.method || "")) {
    const csrfToken = document.cookie
      .split("; ")
      .find((row) => row.startsWith("__Host-csrf="))
      ?.split("=")[1];
    
    if (csrfToken) {
      config.headers["X-CSRF-Token"] = csrfToken;
    }
  }
  return config;
});

// Handle 403 — refresh CSRF token and retry
api.interceptors.response.use(
  (res) => res,
  async (error) => {
    if (error.response?.status === 403 && !error.config._retry) {
      error.config._retry = true;
      await api.get("/auth/csrf-token"); // Refreshes cookie
      return api(error.config);
    }
    return Promise.reject(error);
  }
);

export default api;

Step 5: Next.js Complete Setup

// lib/csrf.ts
import { cookies } from "next/headers";
import crypto from "crypto";

export function generateCsrfToken(): string {
  return crypto.randomBytes(32).toString("hex");
}

export function setCsrfCookie(token: string) {
  cookies().set("__Host-csrf", token, {
    httpOnly: false,
    secure: true,
    sameSite: "strict",
    path: "/",
    maxAge: 3600,
  });
}

export function validateCsrf(request: Request): boolean {
  const cookieStore = cookies();
  const cookieToken = cookieStore.get("__Host-csrf")?.value;
  const headerToken = request.headers.get("x-csrf-token");
  
  if (!cookieToken || !headerToken) return false;
  
  try {
    return crypto.timingSafeEqual(
      Buffer.from(cookieToken),
      Buffer.from(headerToken)
    );
  } catch {
    return false;
  }
}

Step 6: Automated CSRF Testing

// tests/csrf.test.ts
import { describe, it, expect } from "vitest";

describe("CSRF Protection", () => {
  it("rejects POST without CSRF token", async () => {
    const res = await fetch("/api/action", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ data: "test" }),
    });
    expect(res.status).toBe(403);
  });
  
  it("rejects POST with mismatched Origin", async () => {
    const res = await fetch("/api/action", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Origin": "https://evil.com",
      },
      body: JSON.stringify({ data: "test" }),
    });
    expect(res.status).toBe(403);
  });
  
  it("accepts POST with valid CSRF token", async () => {
    // Get token
    const tokenRes = await fetch("/api/auth/csrf-token");
    const { csrfToken } = await tokenRes.json();
    
    const res = await fetch("/api/action", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-Token": csrfToken,
        "Cookie": `__Host-csrf=${csrfToken}`,
      },
      body: JSON.stringify({ data: "test" }),
    });
    expect(res.status).toBe(200);
  });
  
  it("allows GET without CSRF token", async () => {
    const res = await fetch("/api/data");
    expect(res.status).toBe(200);
  });
});

11. Security Tooling Comparison

ToolPurposeCSRF RelevanceLearning CurveCI/CDEnterprise
csurf (deprecated)Express CSRF middlewareDirectLow❌ (unmaintained)
csrf-csrfModern double-submit CSRFDirectLow
lusitaniaCSRF token libraryDirectLow
Auth.js / NextAuthAuthentication frameworkSession + OAuth CSRFMedium
OWASP ZAPDAST scannerDetects missing CSRFMedium
Burp SuitePenetration testingCSRF testing + bypassHigh✅ (Pro)
SemgrepSAST (code patterns)Finds missing protectionsLow
SnykDependency scanningVulnerable auth librariesLow
Helmet.jsSecurity headersIndirect (CSP, etc.)Low
Authentication:  Auth.js (NextAuth) or custom with CSRF middleware
CSRF Library:    csrf-csrf (double-submit) or custom middleware
Headers:         Helmet.js
SAST:            Semgrep rules for CSRF patterns
DAST:            OWASP ZAP in CI pipeline
Dependencies:    Snyk or Dependabot
Manual Testing:  Burp Suite (penetration testing)

12. Cheatsheet

CSRF Attack Quick Reference

VectorPayloadRequires JS?Bypasses SameSite=Lax?
Auto-submit form (POST)<form action="..." method="POST"> + onload=submitOptional
Image tag (GET)<img src="https://target/action">No
Link click (GET navigation)<a href="https://target/action">No✅ (top-level)
fetch + credentialsfetch(url, {credentials: "include"})Yes
Form to iframe<form target="hidden-iframe">Optional
Set-Cookie: __Host-session=<value>;
  Secure;
  HttpOnly;
  SameSite=Lax;
  Path=/;
  Max-Age=3600

Defense Quick Reference

// Minimum viable CSRF protection (modern apps)
// 1. Set SameSite=Lax on all auth cookies ✓
// 2. Validate Origin header on POST/PUT/DELETE ✓
// 3. Require Content-Type: application/json (triggers preflight) ✓
// 4. Add CSRF token for form submissions ✓
// 5. Never perform state changes on GET ✓

Common Anti-Patterns

Anti-PatternWhy It’s WrongFix
CSRF token in URL query paramLeaks via Referer header, logsUse header or POST body
Predictable tokensAttacker can guessUse crypto.randomBytes(32)
Token not bound to sessionToken reuse across sessionsRegenerate on auth
GET endpoints with side effectsImage/link CSRF bypasses everythingUse POST for mutations
SameSite=None without reasonOpts out of browser protectionUse Lax minimum
Trusting Referer aloneCan be strippedUse Origin + Referer fallback
Access-Control-Allow-Origin: * with credentialsDoesn’t work (browser rejects), but indicates misconfigurationExplicit origin list

Secure Fetch Patterns

// ✅ Safe: JSON API with custom header
fetch("/api/action", {
  method: "POST",
  credentials: "same-origin",
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-Token": getCsrfToken(),
  },
  body: JSON.stringify(data),
});

// ❌ Dangerous: No protection
fetch("/api/action", {
  method: "POST",
  credentials: "include",
  body: formData, // No Content-Type header set = no preflight
});

13. Real-World Engineering Mindset

Banking/Payment Systems

Problem: Financial transactions are the highest-value CSRF target.

Strategy:

  1. SameSite=Strict on session cookies (accept UX trade-off)
  2. CSRF tokens (synchronizer pattern — server-side binding)
  3. Re-authentication for transactions above threshold
  4. Transaction signing (separate confirmation step)
  5. Velocity checks and anomaly detection
  6. Out-of-band confirmation (SMS/push for high-value)

What a senior security engineer would choose:

Internal Admin Dashboards

Problem: Admin panels have highest privilege but often weakest protection.

Strategy:

  1. SameSite=Strict (internal tools can afford UX cost)
  2. Separate authentication domain from public-facing app
  3. Short session timeouts (15-30 min)
  4. IP allowlisting at network layer
  5. Audit logging on all state changes
  6. MFA on session and critical actions

Hidden pitfall: Internal tools often skip CSRF protection because “it’s internal” — but compromised employee machines, XSS on any internal page, or phishing links can still exploit this.

Multi-Region Authentication

Problem: Session replication across regions adds latency; CSRF tokens must be globally consistent.

Strategy:

  1. Stateless tokens (HMAC-based double-submit) — no session store dependency
  2. JWT sessions with regional validation
  3. CSRF token derived from session: HMAC(session_id, secret) — consistent across regions
  4. Sticky sessions at CDN level for form-heavy flows

14. Brainstorm / Open Questions

Browser Networking (15 questions)

  1. Why do browsers attach cookies to requests they didn’t originate?
  2. How does a browser decide whether a request is “same-site”?
  3. What happens to SameSite cookies during a redirect chain?
  4. Can a service worker bypass SameSite restrictions?
  5. How does browser cookie partitioning (CHIPS) affect CSRF?
  6. What is the difference between a “navigation” and a “subresource” request?
  7. Why does the browser send cookies on form submissions but not always on fetch?
  8. How does the 2-minute Lax+POST exception work in Chrome’s cookie model?
  9. Can WebSocket connections be used for CSRF attacks?
  10. How do browsers handle cookies during HTTP→HTTPS redirects?
  11. What role does the Sec-Fetch-Site header play in CSRF defense?
  12. How would a browser implement SameSite checking internally?
  13. What happens to credentials during a cross-origin redirect?
  14. How does Private Network Access specification interact with CSRF?
  15. Can <link rel="preload"> trigger CSRF?

Authentication (15 questions)

  1. When should you use cookie-based auth vs bearer tokens?
  2. How does session fixation relate to CSRF?
  3. What’s the CSRF implication of storing refresh tokens in cookies?
  4. How should CSRF protection differ between SSR and SPA auth?
  5. Can a BFF pattern eliminate CSRF concerns entirely?
  6. How does token rotation affect CSRF token validity?
  7. Should API keys be sent as cookies or headers?
  8. How does OAuth’s implicit flow create CSRF risks?
  9. What’s the CSRF risk of “remember me” functionality?
  10. How should passwordless auth (magic links) handle CSRF?
  11. Does WebAuthn/passkeys eliminate CSRF?
  12. How should CSRF protection work with sliding session windows?
  13. What’s the security difference between session ID rotation and CSRF token rotation?
  14. How does federated authentication (SAML) handle CSRF?
  15. Should logout endpoints require CSRF protection?

Cookies (15 questions)

  1. Why is __Host- prefix more secure than __Secure-?
  2. What happens when two cookies have the same name but different paths?
  3. How can a subdomain override parent domain cookies?
  4. What’s the maximum safe cookie size for CSRF tokens?
  5. How does cookie order affect CSRF validation?
  6. Can cookies be set via HTTP response during a CORS request?
  7. What’s the security implication of Domain=.example.com vs no Domain?
  8. How do third-party cookie restrictions affect CSRF?
  9. Can cookies be exfiltrated via CSS injection without XSS?
  10. What’s the difference between persistent and session cookies for CSRF?
  11. How does Safari’s ITP affect cookie-based CSRF defenses?
  12. Can a 302 redirect set cookies that bypass SameSite?
  13. What happens to cookies when a browser is in private/incognito mode?
  14. How should cookie attributes differ between development and production?
  15. Can a CORS preflight response set cookies?

SameSite (15 questions)

  1. Why does SameSite=Lax allow top-level GET navigations?
  2. What’s the CSRF risk remaining with SameSite=Lax?
  3. When is SameSite=Strict absolutely necessary?
  4. How does schemeful same-site change the security model?
  5. What are the edge cases where SameSite=Lax doesn’t protect?
  6. How do pop-up windows interact with SameSite?
  7. What’s the SameSite behavior during OAuth redirects?
  8. Can SameSite be bypassed via browser bugs?
  9. How does SameSite interact with iframe navigation?
  10. What’s the difference between “same-site” in SameSite cookies vs Sec-Fetch-Site?
  11. How does SameSite work with WebSocket upgrade requests?
  12. What happens when SameSite cookies meet server-side redirects?
  13. Should SameSite=None ever be used? What are legitimate cases?
  14. How does Android WebView handle SameSite?
  15. What’s the SameSite behavior for <form method="GET">?

OAuth Security (15 questions)

  1. Why is the state parameter necessary even with SameSite cookies?
  2. How does PKCE protect against authorization code interception?
  3. What’s the CSRF risk in the OAuth implicit flow?
  4. Can login CSRF bypass SameSite=Lax?
  5. How should a SPA validate OAuth state without server-side sessions?
  6. What’s the CSRF implication of OAuth token refresh?
  7. How does OIDC’s nonce differ from OAuth’s state?
  8. Can an OAuth provider be used as a CSRF vector against the relying party?
  9. What’s the security risk of using localStorage for OAuth state?
  10. How should OAuth callback endpoints validate the request?
  11. Can PKCE replace the state parameter for CSRF?
  12. What happens if the OAuth state is predictable?
  13. How does OpenID Connect’s front-channel logout create CSRF risks?
  14. What’s the CSRF implication of silent token renewal via iframe?
  15. How should federated logout handle CSRF across identity providers?

SSR Authentication (15 questions)

  1. How should Next.js middleware handle CSRF for API routes?
  2. What’s the CSRF implication of Server Actions in Next.js?
  3. How does Astro’s server-side rendering affect CSRF token delivery?
  4. Should SSR apps use synchronizer tokens or double-submit?
  5. How does edge rendering (Cloudflare Workers) affect session management?
  6. What’s the CSRF risk of SSR hydration mismatches?
  7. How should streaming SSR handle CSRF tokens?
  8. Can React Server Components be CSRF-attacked?
  9. How does ISR (Incremental Static Regeneration) interact with CSRF?
  10. What’s the CSRF risk of Next.js revalidation endpoints?
  11. How should CSRF tokens work with partial prerendering?
  12. What’s the session architecture for hybrid SSR/SPA apps?
  13. How does Remix’s form handling affect CSRF defense?
  14. Should GraphQL queries in SSR be CSRF-protected?
  15. How does server-side cookie forwarding affect CSRF in BFF patterns?

API Security (15 questions)

  1. Should REST APIs that only accept JSON need CSRF protection?
  2. How does GraphQL’s single endpoint affect CSRF defense strategy?
  3. What’s the CSRF risk of Server-Sent Events (SSE)?
  4. How should WebSocket connections handle CSRF during handshake?
  5. Should internal APIs behind API gateways need CSRF protection?
  6. How does gRPC-web handle CSRF?
  7. What’s the CSRF risk of file upload endpoints?
  8. How should CSRF work with API versioning?
  9. Should webhook endpoints have CSRF protection?
  10. How does tRPC handle CSRF for mutations?
  11. What’s the CSRF risk of GraphQL subscriptions?
  12. How should batch API endpoints validate CSRF?
  13. Should rate limiting interact with CSRF validation?
  14. How does API gateway origin validation differ from app-level?
  15. What’s the CSRF implication of JSONP endpoints?

Enterprise AppSec (15 questions)

  1. How should CSRF governance work in a microservices architecture?
  2. What’s the organizational responsibility model for CSRF (platform vs product)?
  3. How should CSRF incidents be detected and responded to?
  4. What CSRF metrics should security teams monitor?
  5. How should CSRF protection be tested in CI/CD?
  6. What’s the migration strategy from no CSRF protection to full protection?
  7. How should CSRF exceptions/bypasses be governed?
  8. What’s the CSRF strategy for third-party integrations?
  9. How should CSRF protection evolve with browser API changes?
  10. What’s the cost-benefit of each CSRF defense layer?
  11. How should security reviews evaluate CSRF in new features?
  12. What’s the CSRF implication of A/B testing infrastructure?
  13. How should CSRF protection work across deployment environments?
  14. What’s the disaster recovery plan for CSRF token infrastructure?
  15. How should CSRF findings be prioritized in bug bounty programs?

Browser Behavior (5 additional questions)

  1. How do browser extensions affect CSRF security models?
  2. Can Developer Tools bypass SameSite for testing?
  3. How does browser prefetching (<link rel="prefetch">) interact with CSRF?
  4. What’s the CSRF implication of browser back/forward cache (bfcache)?
  5. How do browsers handle CSRF defense during network errors and retries?

15. Practice Questions

Beginner (35 questions)

Q1. What does CSRF stand for?

Q2. True or False: CSRF attacks require the attacker to know the victim’s password.

Q3. Which HTTP methods should NEVER perform state-changing operations?

Q4. What browser behavior makes CSRF possible?

Q5. Which cookie attribute directly prevents cross-site cookie sending?

Q6. True or False: An attacker needs to inject JavaScript into the target site to perform CSRF.

Q7. What is the default SameSite value in modern browsers when not explicitly set?

Q8. Which HTML element can trigger a GET-based CSRF without any user interaction?

Q9. True or False: SameSite=Lax cookies are sent when a user clicks a cross-site link.

Q10. What is the purpose of a CSRF token?

Q11. Which of these is NOT a CSRF defense?

Q12. True or False: The Same-Origin Policy prevents CSRF attacks.

Q13. What happens when a hidden form auto-submits to a banking site?

Q14. Which SameSite value provides the strongest CSRF protection?

Q15. What does HttpOnly on a cookie prevent?

Q16. True or False: A form submission from evil.com to bank.com is blocked by CORS.

Q17. What is the relationship between sessions and CSRF?

Q18. Which content type does NOT trigger a CORS preflight?

Q19. True or False: CSRF can only work if the victim is currently logged into the target site.

Q20. What is “login CSRF”?

Q21–Q35: (Additional beginner questions covering form methods, cookie scoping, request types, basic defenses, and browser behavior fundamentals)

Q21. What’s the key difference between CSRF and XSS?

Q22. True or False: Adding credentials: "include" to a fetch request creates CSRF vulnerability.

Q23. Which of these can an attacker site do WITHOUT user interaction?

Q24. What is the “origin” of a request from https://app.example.com:8080/page?

Q25. True or False: A cookie set with Domain=.example.com is sent to all subdomains.

Q26. What does the Secure cookie attribute do?

Q27. Why can’t an attacker read a CSRF token from another origin?

Q28. True or False: CSRF tokens should be regenerated on every request.

Q29. What type of CSRF attack uses <img src="...">?

Q30. Which header do browsers ALWAYS send on cross-origin POST requests?

Q31. True or False: SameSite=None requires the Secure attribute.

Q32. What’s the purpose of the __Host- cookie prefix?

Q33. Can a CSRF attack steal data from the target site?

Q34. What is the difference between “same-site” and “same-origin”?

Q35. True or False: REST APIs that only accept Content-Type: application/json are immune to form-based CSRF.


Junior (35 questions)

Q36. How does the double-submit cookie pattern work?

Q37. Your React app uses fetch("/api/action", { credentials: "same-origin" }). Is it vulnerable to CSRF from evil.com?

Q38. True or False: Content-Type: application/json on a fetch request triggers CORS preflight.

Q39. What’s wrong with this CSRF token implementation?

const token = Math.random().toString(36).substring(2);

Q40. Your API returns CORS header Access-Control-Allow-Origin: *. Can CSRF still occur?

Q41. When should you use SameSite=Strict vs SameSite=Lax?

Q42. True or False: If an API requires the Authorization: Bearer <token> header, it’s immune to CSRF.

Q43. Your Express app uses csurf middleware. A request fails with “invalid csrf token”. What should you check?

Q44. How does Origin header validation prevent CSRF?

Q45. What’s the vulnerability in this code?

app.get("/api/delete-user/:id", authenticateUser, (req, res) => {
  deleteUser(req.params.id);
  res.json({ success: true });
});

Q46. True or False: CSRF tokens should be transmitted over HTTPS to prevent interception.

Q47. An OAuth login flow doesn’t use the state parameter. What’s the risk?

Q48. What’s the difference between a CSRF token in a hidden form field vs a custom header?

Q49. Your React app stores the JWT in localStorage and sends it via Authorization header. Is CSRF a concern?

Q50–Q70: (Additional junior questions covering React patterns, axios configuration, OAuth basics, token delivery, CORS interaction, and practical debugging)

Q50. True or False: A <form> tag can set custom request headers.

Q51. What’s the CSRF risk of withCredentials: true in axios?

Q52. How do you deliver a CSRF token to a React SPA?

Q53. True or False: Preflight requests (OPTIONS) include cookies.

Q54. What makes text/plain content type dangerous for CSRF?

Q55. Your CSRF token validation uses string comparison (===). What’s the security risk?


Senior (35 questions)

Q71. Design a CSRF protection strategy for a Next.js App Router application that uses Server Actions, API routes, and edge middleware.

Q72. Your enterprise app has 50 microservices behind an API gateway. Where should CSRF validation happen?

Q73. True or False: GraphQL APIs are inherently protected from CSRF because they use POST.

Q74. A security audit finds that your SPA sets SameSite=None; Secure on session cookies for cross-site iframe embedding. How do you maintain CSRF protection?

Q75. How should CSRF protection differ for a real-time WebSocket-based application?

Q76. Your production monitoring shows CSRF token validation failures spiking. How do you investigate?

Q77. Design session architecture for an application that runs on both app.example.com and admin.example.com.

Q78. True or False: Server-side rendering eliminates the need for CSRF tokens if using SameSite=Lax.

Q79. How do you implement CSRF protection for a multi-region application where sessions are stored in different regional databases?

Q80. A penetration tester claims they can bypass your CSRF protection using a Flash file. Is this still relevant?

Q81–Q105: (Additional senior questions covering edge auth, production debugging, enterprise governance, OAuth security, and advanced architecture patterns)

Q81. How should Cloudflare Workers handle CSRF for edge-rendered pages?


Expert / Browser Security Engineer (35 questions)

Q106. Explain how a browser implements SameSite cookie checking internally. What data structures and request metadata are involved?

Q107. A researcher discovers that during a 302 redirect from evil.com → bank.com, SameSite=Lax cookies are sent. Is this a browser bug?

Q108. How could browser cookie partitioning (CHIPS - Cookies Having Independent Partitioned State) affect CSRF defense strategies?

Q109. Design a CSRF protection mechanism that works even if XSS is present on the page.

Q110. How does the Sec-Fetch-* header family interact with CSRF defense, and should servers rely on it?

Q111. Explain how an attacker could exploit a CSRF vulnerability in combination with an open redirect to bypass Origin validation.

Q112–Q140: (Additional expert questions covering browser networking internals, advanced bypass techniques, specification-level analysis, and large-scale architecture decisions)

Q112. True or False: A 307 redirect preserves the HTTP method and body, while a 302 typically converts POST to GET.

Q113. How would you design a CSRF-immune authentication system for a browser-based application from scratch?


16. Personalized Recommendations

For Your Stack (React + Next.js + Astro + Vite + TypeScript)

Priority CSRF Concepts

  1. Immediate: SameSite cookie configuration for Next.js sessions
  2. Immediate: Origin validation middleware in Next.js/Astro API routes
  3. High: Understanding how Server Actions handle CSRF (built-in protection)
  4. High: CSRF token delivery pattern for React SPAs
  5. Medium: OAuth state/PKCE implementation for social login
  6. Medium: Cookie architecture for SSR vs client-side rendering
  7. Advanced: Edge rendering (Vercel Edge) session implications
  8. Advanced: Multi-tenant CSRF isolation

Common Authentication Mistakes

MistakeImpactFix
JWT in localStorageXSS → token thefthttpOnly cookie + CSRF token
No CSRF on API routesCSRF via form/fetchMiddleware validation
SameSite not set explicitlyBrowser-dependent behaviorAlways set Lax minimum
GET endpoints with side effectsImage/link CSRFPOST for all mutations
Shared session across subdomainsCross-subdomain CSRF__Host- prefix
No Origin validationBasic CSRF possibleMiddleware check
OAuth without stateLogin CSRFAlways use state + PKCE

60-Day Learning Plan

Week 1-2: Foundations

Week 3-4: Framework Integration

Week 5-6: OAuth & Authentication Architecture

Week 7-8: Advanced & Enterprise

Ongoing:


Beginner

ResourceURL
OWASP CSRF Attack Descriptionhttps://owasp.org/www-community/attacks/csrf
MDN: Using HTTP Cookieshttps://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
MDN: SameSite Cookieshttps://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
web.dev: SameSite Cookies Explainedhttps://web.dev/samesite-cookies-explained/
MDN: Same-Origin Policyhttps://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
PortSwigger: CSRFhttps://portswigger.net/web-security/csrf

Intermediate

ResourceURL
OWASP CSRF Prevention Cheat Sheethttps://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
MDN: CORShttps://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
Auth.js (NextAuth)https://authjs.dev
OAuth 2.0https://oauth.net/2/
OpenID Connecthttps://openid.net/connect/
MDN: Fetch API credentialshttps://developer.mozilla.org/en-US/docs/Web/API/fetch
Chrome: SameSite Updateshttps://www.chromium.org/updates/same-site/

Advanced

ResourceURL
RFC 6749: OAuth 2.0https://datatracker.ietf.org/doc/html/rfc6749
RFC 7636: PKCEhttps://datatracker.ietf.org/doc/html/rfc7636
Chromium SameSite FAQhttps://www.chromium.org/updates/same-site/faq
Fetch Metadata (Sec-Fetch-*)https://web.dev/fetch-metadata/
OWASP Testing Guide: CSRFhttps://owasp.org/www-project-web-security-testing-guide/
PortSwigger: Bypassing SameSitehttps://portswigger.net/web-security/csrf/bypassing-samesite-restrictions

Expert / Browser Internals

ResourceURL
Chromium Cookie Source Codehttps://source.chromium.org/chromium/chromium/src/+/main:net/cookies/
HTML Spec: Originhttps://html.spec.whatwg.org/multipage/origin.html
Fetch Spechttps://fetch.spec.whatwg.org/
Cookie Spec (RFC 6265bis)https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis
W3C Fetch Metadatahttps://www.w3.org/TR/fetch-metadata/
Chromium Network Stackhttps://source.chromium.org/chromium/chromium/src/+/main:net/

18. Advanced Engineering Topics

Browser Credential Behavior Internals

The browser’s cookie-sending decision follows this algorithm:

function shouldSendCookie(cookie, request):
  1. Domain match: cookie.domain matches request URL
  2. Path match: cookie.path is prefix of request path
  3. Secure check: if cookie.secure, request must be HTTPS
  4. HttpOnly check: if cookie.httpOnly and request from JS, block read
  5. SameSite check:
     a. Compute request's "site for cookies" from initiator
     b. Compute target site from request URL
     c. If cross-site:
        - Strict: block
        - Lax: allow only if top-level navigation + safe method (GET)
        - None: allow (requires Secure)
  6. Partition check (CHIPS): if partitioned, must match top-level partition key

Future Browser Authentication Directions

Secure-by-Default Platform Design

For platform teams building internal frameworks:

// Framework-level CSRF protection (team doesn't need to think about it)
// Every route handler automatically gets CSRF validation

// platform/createApiHandler.ts
export function createApiHandler(handler: Handler) {
  return async (req: Request) => {
    // Automatic: skip for safe methods
    if (!["GET", "HEAD", "OPTIONS"].includes(req.method)) {
      // Automatic: Origin validation
      if (!validateOrigin(req)) {
        return new Response("Forbidden", { status: 403 });
      }
      // Automatic: CSRF token validation
      if (!validateCsrfToken(req)) {
        return new Response("Invalid CSRF", { status: 403 });
      }
    }
    // Automatic: set secure cookie defaults
    const res = await handler(req);
    enforceSecureCookieDefaults(res);
    return res;
  };
}

Summary

Key Takeaways

  1. CSRF exploits browser automatic credential attachment — understand this mental model deeply
  2. SameSite=Lax is the most impactful single defense — modern browsers default to it
  3. Defense-in-depth is essential: SameSite + Origin validation + CSRF tokens + proper CORS
  4. Authentication architecture determines CSRF surface — cookie-based auth needs CSRF protection, bearer token auth doesn’t
  5. SPAs are NOT immune — if using cookies for auth, CSRF applies
  6. OAuth needs its own CSRF protection — always use state + PKCE
  7. Server Actions / RSC have built-in protections — but understand what they protect and what they don’t

Next Steps

  1. Audit your current applications for CSRF vulnerabilities
  2. Implement SameSite + Origin validation as minimum baseline
  3. Add automated CSRF testing to your CI/CD pipeline
  4. Study OAuth security deeply (state, PKCE, redirect validation)
  5. Practice attacking your own apps to build attacker mindset

Advanced Topics to Continue Learning


Edit page
Share this post:

Previous Post
Content Security Policy (CSP)
Next Post
Docker & Docker Compose — Complete Deep-Dive Engineering Guide