Cross-Site Request Forgery: a malicious page tricks the user's browser into making a request to your site, where the user is already logged in. The browser cheerfully attaches the session cookie; your server runs the action — transfer money, change email, delete account — as if the user wanted it. The attacker never touched the user's credentials.
← Back to Securityyour-bank.com. They visit evil.com. evil.com contains a hidden form or image whose URL is your-bank.com/transfer?to=attacker&amount=1000.your-bank.com with that request automatically — that's how cookies have always worked.evil.com — unless it's checking specifically.<!-- on evil.com -->
<form action="https://your-bank.com/transfer"
method="POST" id="f">
<input name="to" value="attacker"/>
<input name="amount" value="1000"/>
</form>
<script>document.getElementById('f').submit();</script>
Victim loads the page; the browser submits the form to the bank with the bank's cookies; transfer goes through. No interaction beyond visiting the malicious page.
If GET /unsubscribe?id=42 changes state, an attacker can trigger it with <img src="https://your-app/unsubscribe?id=42">. The image "fails to load" but the request fired with cookies attached. Rule: GET must be safe — never have side effects.
"We accept JSON, so we're safe." Not always — if the server accepts application/x-www-form-urlencoded too (common Spring/Express defaults), a form post with a payload that looks like JSON works. Lock down content-type expectations and reject non-JSON for JSON endpoints.
The biggest shift in CSRF defense in years. SameSite=Lax (the modern browser default) means cookies aren't attached to most cross-site requests. SameSite=Strict excludes even top-level navigations. With Lax/Strict, classic CSRF largely fails.
Caveats:
SameSite=None + Secure; those need other CSRF defenses.The server generates a random token per session (or per form), stores it server-side, and embeds it in every form/request as a hidden field or header. The server rejects any state-changing request whose token doesn't match. Cross-site attackers can't read the token (same-origin policy stops them), so they can't forge a valid request.
Frameworks ship this. Spring Security CsrfFilter, Django {% csrf_token %}, Rails protect_from_forgery, ASP.NET AntiForgeryToken. Use them; don't roll your own.
For stateless APIs: send a random token in both a cookie and a request header. The server checks they match. Cross-site attackers can read neither, so they can't forge the match. Common with SPA + JWT setups.
Browsers don't allow cross-origin requests with custom headers (without a CORS preflight that your server controls). Require a header like X-Requested-With: XMLHttpRequest or X-CSRF-Token for state-changing endpoints; reject otherwise. Simple, effective for SPAs and JSON APIs.
Modern browsers send Origin on POST. Reject requests whose Origin isn't your own site. Belt-and-braces with SameSite cookies; both can be defeated alone, but together they cover real-world cases.
Bank transfers, password changes, email changes, billing updates — require the user to re-enter their password or pass an MFA challenge. Even a successful CSRF can't satisfy that.
Browser defaults have done most of the work for the common case:
The shifted attack surface: CSRF mostly survives now in cases requiring SameSite=None (third-party embeds, federated login flows), in legacy apps without SameSite set, and in mobile/desktop apps where browser CSRF defenses don't apply. Enable framework CSRF protection and SameSite together — they cover different cases.