Introduction

Cross-Site Request Forgery (CSRF), also known as XSRF or "sea-surf," is an attack that forces an authenticated user to execute unwanted actions on a web application in which they are currently logged in. Unlike Cross-Site Scripting (XSS), which exploits the trust a user has in a particular site, CSRF exploits the trust that a site has in a user's browser.

The attack leverages a fundamental behavior of web browsers: cookies associated with a domain are automatically included with every request to that domain, regardless of where the request originates. If a user is logged into their bank at bank.com, and they visit a malicious page on evil.com, any request that evil.com triggers to bank.com will automatically include the user's session cookies -- making the forged request indistinguishable from a legitimate one.

CSRF attacks are particularly insidious because the victim may never realize they have been attacked. The forged request executes with the victim's full privileges, and the resulting action (transferring money, changing an email address, granting permissions) appears to have been performed by the victim themselves.

"CSRF is the sleeping giant of web application security. It is less well known than XSS but potentially far more dangerous, because it allows attackers to perform actions that the user never intended." -- Bill Zeller and Edward Felten, Princeton University, in their seminal 2008 paper on CSRF

How CSRF Works

A CSRF attack requires three conditions to be present simultaneously:

  1. The victim is authenticated to the target application (has an active session cookie).
  2. The application relies solely on cookies for session identification and does not require any additional proof that the request was intentional.
  3. The attacker can predict all request parameters. If any parameter contains a value the attacker cannot determine (such as a CSRF token), the attack fails.
# CSRF attack flow:1. Victim logs into bank.com - Browser receives: Set-Cookie: session=abc123; HttpOnly; Secure2. Victim visits evil.com (in a new tab or via phishing link)3. evil.com contains a hidden form: <form action="https://bank.com/transfer" method="POST" id="csrf-form"> <input type="hidden" name="to" value="attacker_account"> <input type="hidden" name="amount" value="10000"> </form> <script>document.getElementById('csrf-form').submit();</script>4. Browser automatically includes bank.com cookies with the request POST /transfer HTTP/1.1 Host: bank.com Cookie: session=abc123 Content-Type: application/x-www-form-urlencoded to=attacker_account&amount=100005. bank.com sees a valid session cookie and processes the transfer

The server cannot distinguish this forged request from a legitimate one because the session cookie -- the sole authentication mechanism -- is present and valid. The request contains all the correct parameters, comes from an authenticated session, and appears entirely normal from the server's perspective.

Attack Vectors

GET-Based CSRF

The simplest form of CSRF exploits applications that use GET requests for state-changing operations. This violates the HTTP specification (RFC 7231), which states that GET requests should be safe and idempotent, but many applications still use GET for actions like deleting records or changing settings.

<!-- Attacker embeds this image tag on any page the victim might visit --><img src="https://bank.com/transfer?to=attacker&amount=5000" width="0" height="0"><!-- Or hides it in a forum post, email, or advertisement --><img src="https://admin.example.com/users/delete?id=42" style="display:none"><!-- The browser automatically makes the GET request to load the "image" --><!-- The cookies for bank.com or admin.example.com are sent along -->

GET-based CSRF is trivially easy to exploit because images, iframes, and other resources are loaded automatically by browsers without any user interaction beyond visiting the page. This is why state-changing operations must never be performed via GET requests.

POST-Based CSRF

POST-based CSRF requires the attacker to construct a hidden form and use JavaScript to submit it automatically. While slightly more complex than GET-based attacks, it is still straightforward to implement and equally effective against unprotected applications.

<!-- Auto-submitting hidden form --><body onload="document.getElementById('f').submit()"><form id="f" action="https://example.com/account/email" method="POST"> <input type="hidden" name="email" value="attacker@evil.com"></form></body><!-- For JSON APIs, the attacker can use fetch with no-cors mode --><!-- (limited but sometimes sufficient for CSRF) --><script>fetch('https://api.example.com/settings', { method: 'POST', mode: 'no-cors', credentials: 'include', headers: { 'Content-Type': 'text/plain' }, body: '{"email":"attacker@evil.com"}'});</script>

Synchronizer Token Pattern

The synchronizer token pattern is the most widely deployed CSRF defense. The server generates a unique, unpredictable token for each user session (or for each form/request), embeds it as a hidden field in forms, and validates it on submission. Since the attacker cannot read the token from a cross-origin context (blocked by the same-origin policy), they cannot include it in their forged request.

# Server-side (Python/Flask example)import secrets@app.before_requestdef csrf_protect(): if request.method == 'POST': token = session.get('csrf_token') submitted = request.form.get('csrf_token') if not token or token != submitted: abort(403, 'CSRF token validation failed')@app.before_requestdef generate_csrf_token(): if 'csrf_token' not in session: session['csrf_token'] = secrets.token_hex(32)# Template (Jinja2)<form method="POST" action="/transfer"> <input type="hidden" name="csrf_token" value="{{ session.csrf_token }}"> <input name="to" placeholder="Recipient"> <input name="amount" placeholder="Amount"> <button type="submit">Transfer</button></form>
PropertyRequirementRationale
UniquenessOne token per session minimumPrevents token reuse across sessions
UnpredictabilityGenerated with CSPRNGAttacker cannot guess valid tokens
LengthAt least 128 bits of entropyMakes brute force infeasible
BindingTied to user sessionToken from one session cannot be used in another
ValidationConstant-time comparisonPrevents timing side-channel attacks

Double Submit Cookie

The double submit cookie pattern is a stateless alternative to the synchronizer token pattern. Instead of storing the CSRF token in the server-side session, the server sets the token as a cookie and also requires the client to include the same value in a request parameter or header. The server then verifies that the cookie value and the submitted value match.

This works because the same-origin policy prevents an attacker from reading or setting cookies for the target domain. While the browser will automatically include the CSRF cookie with the forged request, the attacker cannot read its value to include it as a form parameter or custom header.

// Double submit cookie implementation (Express.js)// Middleware: set CSRF cookie if not presentapp.use((req, res, next) => { if (!req.cookies.csrfToken) { const token = crypto.randomBytes(32).toString('hex'); res.cookie('csrfToken', token, { httpOnly: false, // JavaScript must be able to read it secure: true, sameSite: 'Strict' }); } next();});// Middleware: validate CSRF on state-changing requestsapp.use((req, res, next) => { if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) { const cookieToken = req.cookies.csrfToken; const headerToken = req.headers['x-csrf-token']; if (!cookieToken || cookieToken !== headerToken) { return res.status(403).json({ error: 'CSRF validation failed' }); } } next();});// Client-side: read cookie and include in requestsfetch('/api/transfer', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCookie('csrfToken') }, body: JSON.stringify({ to: 'recipient', amount: 100 })});

The double submit cookie pattern has a known weakness: if the attacker can set cookies on the target domain (through a subdomain cookie injection or a separate vulnerability), they can set both the cookie and the submitted value to a known value. To mitigate this, the server can cryptographically sign the cookie value using HMAC, binding it to the user's session identifier.

SameSite Cookies

The SameSite cookie attribute, standardized in RFC 6265bis, provides browser-level protection against CSRF by controlling when cookies are sent with cross-site requests. It has become one of the most effective defenses against CSRF, and since 2020, most major browsers default to SameSite=Lax for cookies that do not explicitly set the attribute.

SameSite ValueCross-Site POSTCross-Site GET (Top-Level)Cross-Site SubresourceCSRF Protection
StrictCookie NOT sentCookie NOT sentCookie NOT sentStrongest
LaxCookie NOT sentCookie sentCookie NOT sentGood (blocks POST-based CSRF)
NoneCookie sentCookie sentCookie sentNone (requires Secure flag)
# Setting SameSite cookiesSet-Cookie: session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/# SameSite=Strict: maximum protection but may break legitimate flows# Example: clicking a link to bank.com from an email will NOT include# the session cookie, forcing the user to log in again# SameSite=Lax: good balance of security and usability# Cookies are sent with top-level GET navigations (clicking links)# but NOT with POST requests, form submissions, or subresource loads# from cross-site origins# SameSite=None: opt out of protection (must include Secure flag)# Required for legitimate cross-site use cases like embedded widgets

"SameSite cookies are the most significant improvement to web security defaults in the past decade. By making Lax the default, browsers eliminated the majority of CSRF attacks without requiring any changes from web developers." -- Mike West, Google Chrome Security Team

Additional Defenses

Beyond the primary CSRF defenses, several supplementary measures can further reduce risk:

  • Custom request headers: Require a custom header (e.g., X-Requested-With: XMLHttpRequest) for all state-changing API requests. Browsers prevent cross-origin requests from setting custom headers without a CORS preflight, which the target server can reject.
  • Origin and Referer validation: Check the Origin or Referer header to verify that the request originated from the same site. The Origin header is more reliable as it is always sent with POST requests and cannot be spoofed by JavaScript.
  • User interaction confirmation: For high-risk operations (money transfers, password changes), require re-authentication or a CAPTCHA challenge.
  • Short session timeouts: Reduce the window of opportunity for CSRF attacks by expiring sessions more quickly. This must be balanced against user experience.
  • Avoid GET for state changes: Strictly follow HTTP method semantics. GET requests should never modify server state.
// Origin header validation middleware (Express.js)app.use((req, res, next) => { if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) { const origin = req.headers['origin']; const allowedOrigins = ['https://example.com', 'https://www.example.com']; if (!origin || !allowedOrigins.includes(origin)) { return res.status(403).json({ error: 'Invalid origin' }); } } next();});

CSRF vs. XSS

CSRF and XSS are often confused because both involve cross-site interactions, but they exploit fundamentally different trust relationships and have different capabilities and defenses.

AspectCSRFXSS
Trust ExploitedSite trusts the user's browserUser trusts the site's content
Attacker CanPerform actions as the victimRead data, perform actions, inject content
Attacker CannotRead the responseN/A (full access within origin)
RequiresVictim to be authenticatedVulnerable input/output handling
Script ExecutionNo (attacker's script runs on attacker's site)Yes (attacker's script runs on victim's site)
CSRF Tokens HelpYes (primary defense)No (XSS can read the token)

Critically, XSS can be used to defeat CSRF protections. If an attacker has an XSS vulnerability on the target site, they can use JavaScript to read the CSRF token from the page and include it in a forged request. This means that preventing XSS is also essential for CSRF defense. A chain of XSS leading to CSRF bypass is a common attack pattern in real-world penetration tests.

CSRF in Modern Frameworks

Most modern web frameworks include built-in CSRF protection that is enabled by default. Understanding how your framework handles CSRF is essential for ensuring it is properly configured and not accidentally disabled.

# Django: CSRF protection enabled by default# Uses synchronizer token pattern with cookie + hidden field# settings.pyMIDDLEWARE = [ 'django.middleware.csrf.CsrfViewMiddleware', # enabled by default]# Template<form method="POST"> {% csrf_token %} <input name="email" value="..."> <button>Update</button></form># ---# Ruby on Rails: CSRF protection enabled by default# app/controllers/application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception # defaultend# ---# Spring Security (Java): CSRF enabled by default// CsrfFilter is auto-configured// For SPAs, use CookieCsrfTokenRepositoryhttp.csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));# ---# Express.js: requires explicit setup (no built-in CSRF)const csrf = require('csurf');app.use(csrf({ cookie: true }));

Single-page applications (SPAs) that communicate exclusively through JSON APIs with JWT tokens in the Authorization header are naturally resistant to CSRF, because the token is not automatically sent by the browser. However, if tokens are stored in cookies for security against XSS, CSRF protection remains necessary. The choice between cookie storage and header-based tokens involves a fundamental tradeoff between XSS and CSRF risk.

References

  • Zeller, W. and Felten, E. (2008). "Cross-Site Request Forgeries: Exploitation and Prevention." Princeton University Technical Report.
  • OWASP Foundation. (2023). OWASP Cross-Site Request Forgery Prevention Cheat Sheet. OWASP.
  • Barth, A. et al. (2008). "Robust Defenses for Cross-Site Request Forgery." ACM Conference on Computer and Communications Security.
  • West, M. and Goodwin, M. (2016). "Same-Site Cookies." Internet Engineering Task Force (IETF) Draft.
  • Fielding, R. and Reschke, J. (2014). RFC 7231: Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content. IETF.
  • OWASP Foundation. (2021). OWASP Top Ten 2021: A01 Broken Access Control. OWASP.
  • Barth, A. (2011). RFC 6265: HTTP State Management Mechanism. IETF.
  • Czeskis, A. et al. (2010). "Defeating SSO Attacks on Token-Based Authentication Schemes." Workshop on Web 2.0 Security and Privacy.