CSRF Explained — How Cross-Site Request Forgery Works and How to Defend Against It thumbnail

CSRF Explained — How Cross-Site Request Forgery Works and How to Defend Against It

⏱ approx. 22 min views 172 likes 0 LOG_DATE:2026-05-29
TOC

CSRF (Cross-Site Request Forgery) is a vulnerability where the victim — currently logged in to a target web application — is tricked, through a page attacker controls on a different origin, into sending requests they never intended. The essence is the mismatch between "the browser attaches cookies automatically" and "the server treats the presence of a cookie as proof of identity." CSRF often gets confused with XSS, but they're different beasts: XSS is script execution, CSRF is request submission. This article walks through the mechanism, the three flavors (GET / POST / JSON), the Samy and home-router incidents, and the layered defense built from tokens, SameSite cookies, and Origin checks.

01

What CSRF is — abusing automatic cookie attachment #

Browsers bind cookies to origins (scheme + host + port) and attach them automatically to every request to that origin, regardless of which origin the page making the request belongs to. That asymmetry is the starting point of CSRF.

▸ Three conditions for CSRF

(1) the victim is logged in to the target (holds the session cookie); (2) the target authenticates "cookie present = the user"; (3) the target endpoint changes state via a request whose shape an attacker can predict. When those three line up, just having the victim's browser load an attacker page on another origin is enough.

Minimal attack #

A trap page that triggers a money transfer
<!-- attacker.example/lure.html — different origin --> <img src="https://bank.example/transfer?to=attacker&amount=1000000">

# If the victim is logged in to bank.example and opens this page: # the browser issues a GET to the <img> src # the GET carries bank.example's session cookie automatically # bank.example sees "an authenticated user is asking to transfer" → done

The victim notices nothing more than a missing image. "GET endpoints that move money" — at that point the bug is as bad as SQLi.

Difference from XSS #

Aspect XSS CSRF
Does what Runs arbitrary script Sends a fixed-shape request
Runs where Inside the target site's origin On the attacker's site
Read access Yes (everything except HttpOnly, DOM, API responses) No (SOP forbids reading the response)
Impact range Nearly unlimited Limited to state-changing endpoints
Effect of HttpOnly HttpOnly helps HttpOnly is irrelevant (it doesn't touch send-time)

XSS is "execute script," CSRF is "send request." People sometimes describe CSRF as "XSS without the script," but once XSS lands it can also read the CSRF token, so XSS instantly defeats CSRF defenses.

Where CSRF sits in the OWASP Top 10 #

CSRF was a Top-10 regular (A8) from 2003 to 2017, then dropped out in 2017. The reason was SameSite cookies and framework-level CSRF defenses going mainstream. That said, in 2025 it's still routinely found in legacy systems, custom APIs, WebView-based apps. Not a dead bug.

02

Three flavors of CSRF #

CSRF splits by the shape of the forged request. Each flavor has its own defense profile.

GET-based — easiest and most deadly #

<img src>, <script src>, <iframe src>, <link>, window.open all issue GET requests. No user action required — just visiting the attacker's page fires the attack. "Side-effectful GET" (money transfers, account deletions, password changes accepting GET) is a full-on design defect.

POST-based — auto-submitting forms #

Aim a <form>'s action at another origin and submit it from JavaScript.

An auto-submitting form on the attacker's page
<form id="x" action="https://bank.example/transfer" method="POST"> <input name="to" value="attacker"> <input name="amount" value="1000000"> </form> <script>document.getElementById('x').submit();</script>

# POST fires the moment the page loads — auto-attached cookie passes auth

application/x-www-form-urlencoded and multipart/form-data are "simple requests" — they don't trigger a CORS preflight, and that's why CORS alone won't save you.

JSON / custom-header based — protected by CORS #

Use fetch with Content-Type: application/json or a custom header like X-CSRF-Token, and the browser first issues an OPTIONS preflight and refuses to send the actual request unless the server approves it.

So an API that only accepts JSON gets CSRF protection essentially for free. Conversely, if an API accepts both form-encoded and JSON "for convenience," you're leaving the CSRF entry point wide open.

Sub-flavor: Login CSRF #

Force the victim to log in as the attacker's account, then have all subsequent activity (search history, payment info, settings) accumulate in the attacker's account. Google and Yahoo both had incidents. Counter-intuitive bug: even login itself needs CSRF protection.

03

The attacker's playbook #

1. Target selection
Find sites with state-changing endpoints (money transfer, password change, email change, account deletion, settings change).
2. Request analysis
Inspect the legitimate request in DevTools / Burp. GET or POST? Content-Type? Custom headers? CSRF token?
3. Build the trap page
Host HTML on another origin that produces the same shape of request. GET → img tag; POST → auto-submitting form.
4. Lure the victim
Drive traffic via email, social media, ads, SEO. The victim must be logged in to the target while opening the URL, so the targeting matches the target service's audience.
5. Execute
The request fires in the background as the page loads. The victim notices nothing.

The victim's only action is "open the attacker's page." That's also why CSRF incidents — unlike XSS, where the user types an attack string into a field — tend to go undetected for long stretches.

04

Notable CSRF incidents #

The CSRF half of the Samy Worm (MySpace, 2005) #

The Samy worm — famous for XSS — also had a CSRF half. The "victim follows the attacker," "victim's profile HTML gets rewritten" operations were all CSRF against MySpace's state-change endpoints, running in the victim's browser with their own valid cookies. A textbook XSS + CSRF combo.

Home-router rewrites (2008 onward) #

A victim's home router admin page (http://192.168.1.1/) is only reachable from the LAN — but the victim's browser is itself on the LAN. An attacker's web page plants <img src="http://192.168.1.1/setup?dns=8.8.8.x">, and the victim's home DNS settings get flipped to an attacker-controlled DNS. From then on, every device in the house resolving bankexample.com lands on a phishing site. Many Linksys / D-Link / TP-Link models were vulnerable; in Brazil and Colombia, millions of routers were reportedly rewritten. CSRF against LAN-internal devices — a clever extension of the basic attack.

Netflix (2006) #

The old Netflix site had widespread CSRF — adding DVDs to other people's queues, changing shipping addresses, modifying account information all worked from attacker-hosted pages. Researcher Dave Ferguson published the report; Netflix added CSRF tokens to every API within weeks. The moment CSRF graduated from "academic curiosity" to "real-world threat."

YouTube (2008) #

YouTube had CSRF on essentially every state-changing operation: commenting, friending, favoriting, sharing. A Princeton study disclosed it, and it became one of the most-cited cases of "SOP/cookie mismatch left wide open in a major service."

ING Direct (2008) #

Online bank ING Direct was found vulnerable to CSRF for transfers and new-account opening. A turning point in mainstream awareness of CSRF in finance — after this, the industry standardized on CSRF tokens + transaction passwords + SMS auth in combination.

Takeaway #

The thread is: CSRF is unglamorous, but a single state-changing endpoint left unprotected can produce massive damage. The defense has to be 100% across the whole surface — which is exactly why "cookie-layer auto-defense" like SameSite eventually became the standard.

05

Defenses — layered #

SameSite cookies — the modern lead #

Adding the SameSite attribute lets the browser restrict when cookies get attached to cross-origin requests.

Value Behavior
Strict Cookies are never sent on cross-origin requests, including top-level navigation
Lax Sent only on top-level navigation GETs. Not on POST, <img>, <iframe>
None Sent always (the old default). Must be combined with Secure

Chrome defaulted to SameSite=Lax in 2020 (version 80). After that, sites that "do nothing" for CSRF still get the bulk of POST-CSRF defended by browser default. That's the single biggest reason CSRF fell out of the OWASP Top 10.

▸ Still set it explicitly

Don't rely on browser defaults. Safari had a period of not defaulting to SameSite=Lax, and embedded browsers, WebViews, and older browsers may not implement it at all. Always set it explicitly on Set-Cookie.

Recommended session-cookie settings
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/

Strict is the safest, but it has the UX issue of "users arriving from external links look logged out." Convention: auth cookies are Lax, and especially sensitive cookies (e.g. payment-confirmation) are Strict.

CSRF tokens (the Synchronizer Token Pattern) — the classic, reliable #

The server generates a random value, embeds it in the form / response, and demands it back on the request. The attacker's cross-origin page cannot read the token (SOP blocks it), so it can't construct a valid request.

Laravel — embed the token with @csrf
<form method="POST" action="/transfer"> @csrf {{-- Generates --}} <input name="to"> <input name="amount"> </form>

# Server (VerifyCsrfToken middleware) checks _token against the session token

FW Token mechanism
Laravel @csrf, VerifyCsrfToken middleware (on by default)
Django {% csrf_token %}, CsrfViewMiddleware
Rails csrf_meta_tags, protect_from_forgery (on by default)
Spring Security CsrfFilter (on by default)
Express csurf (deprecated in 2022; csrf-csrf replaces it)

For SPAs and pure API backends, the "Double Submit Cookie Pattern" is common — set the token as a cookie, have JS read it and put it in a header. Same-origin JS can read the cookie; cross-origin JS can't (SOP).

Origin / Referer header checks #

The browser automatically attaches an Origin header on POSTs (and similar) and a Referer header in most cases. Verifying them lets you reject anything not from your own site.

Origin check
# PHP $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (!in_array($origin, ['https://app.example'])) { http_response_code(403); exit; }

Origin cannot be forged by an attacker's JavaScript. Referer can be omitted by some privacy settings, so make Origin the primary check and Referer a fallback.

Require a custom header (X-Requested-With, etc.) #

Demanding a custom header like X-Requested-With: XMLHttpRequest forces the browser to do a CORS preflight for cross-origin requests, which the server can reject. Simple, effective, especially popular for JSON APIs.

Re-authenticate sensitive operations #

For things like transfers, password changes, email changes, require password re-entry or MFA every time. The final layer of defense — even if SameSite or tokens are bypassed, damage stays local.

Never make side-effectful GETs #

A design rule: state-changing endpoints must be POST / PUT / DELETE / PATCH. The HTTP spec (RFC 9110) says the same, and it completely shuts down <img src>-style CSRF.

06

Relationship with CORS — frequently confused #

"Enabling CORS opens CSRF up" and "Strict CORS prevents CSRF" are both misconceptions. Let's set them straight.

▸ CORS is about reading; CSRF is about sending

CORS controls whether JavaScript is allowed to read a cross-origin response. For state-changing endpoints, where reading isn't needed, the browser still sends the request itself. So no matter how strict your CORS gets, state-changing CSRF is not prevented.

CORS becomes relevant only when the request is one that triggers a preflight (JSON, custom headers, etc.). The preflight OPTIONS hits the server first, and if the origin is not allowed, the actual request is never sent — so CSRF is incidentally blocked. That's a side effect, not CORS's purpose.

In other words:

  • Form-encoded POST → no preflight → CORS doesn't stop CSRF → token required
  • JSON POST with Content-Type: application/json → preflight → disallowed origins never make the real request → CSRF is incidentally blocked

That's the reasoning behind "make APIs JSON-only" as a CSRF defense.

07

Testing and detection #

Manual testing #

For each state-changing endpoint, actually build and trigger a trap page:

  1. Log in to your site normally
  2. On another origin, host HTML containing <form action="https://target/api/...">
  3. Open the HTML, then check if your site's state changed

If state changed, CSRF lands. A test where "success means vulnerable." Burp Suite's "Generate CSRF PoC" automates exactly this.

Code review #

  • Make sure every state-changing endpoint is protected by CSRF token, SameSite cookie, or Origin check
  • Make sure the framework's CSRF middleware hasn't been excepted on any route (grep Laravel's VerifyCsrfToken::$except etc.)
  • Bearer-token API routes are immune to CSRF, but mixing cookie auth with bearer auth on the same surface opens gaps

Automated scanners #

  • Burp Suite Pro — detects CSRF on state-changing endpoints
  • OWASP ZAP — has rules for missing CSRF tokens
  • Nuclei — CSRF templates for broad sweep checks
08

Related attacks #

Attack Relationship
Clickjacking Overlay invisible buttons on top of iframes and convert a legitimate click into something else. Different from CSRF, but related in that it "weaponizes the victim's authenticated session." Defend with X-Frame-Options / CSP frame-ancestors
SSRF Here, the server is the one issuing requests to attacker-controlled URLs. CSRF uses the browser as a relay; SSRF uses the server
Cross-Site Script Inclusion (XSSI) Read confidential JSON/JSONP from another origin via <script> and observe side effects. A way around CSRF's "cannot read response" constraint
Login CSRF CSRF that logs the victim in as the attacker. Especially problematic in OAuth flows
CSRF on logout CSRF against the logout endpoint — forcibly logs the victim out for UX-DoS
09

Wrap-up — six things every developer should hold #

CSRF has fallen off sharply since SameSite became standard, but legacy code, WebViews, embedded browsers, and mobile WebView SDKs still produce fresh cases. The mindset of layered defense is unchanged.

▸ Six things to hold as a developer
  • Explicitly set SameSite=Lax + HttpOnly + Secure on session cookies
  • Put CSRF tokens or Origin checks on every state-changing endpoint
  • Do not accept side-effectful operations over GET (RFC 9110 principle)
  • Make APIs JSON-only with required application/json so CORS preflights kick in
  • Require re-authentication for transfers, password changes, email changes
  • Grep for CSRF middleware exceptions in the framework and audit them

You can't change the browser's "cookies auto-attach" spec. The whole point of CSRF defense is exactly this: the server must verify "did this request really come from my own site" every single time.