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.
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.
(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 #
<!-- 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.
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.
<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.
The attacker's playbook #
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.
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.
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.
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.
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.
<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.
# 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.
Relationship with CORS — frequently confused #
"Enabling CORS opens CSRF up" and "Strict CORS prevents CSRF" are both misconceptions. Let's set them straight.
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.
Testing and detection #
Manual testing #
For each state-changing endpoint, actually build and trigger a trap page:
- Log in to your site normally
- On another origin, host HTML containing
<form action="https://target/api/..."> - 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::$exceptetc.) - 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
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 |
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.
- Explicitly set
SameSite=Lax+HttpOnly+Secureon 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/jsonso 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.