When browsers made SameSite=Lax the default for cookies around 2020-2021, a lot of teams threw out their CSRF tokens. The attack that classic CSRF protection defended against was suddenly impossible by default. Except… it’s not.
What SameSite=Lax actually does
A Lax cookie is sent with:
- Top-level GET navigations (clicking a link)
- Fetch requests from the same registrable domain
It’s not sent with:
- Cross-site POST from a form
- Cross-site fetch()/XHR
- Cross-site iframes
So the classic CSRF attack — a hidden <form action="https://victim.com/transfer" method="POST"> on attacker.com that auto-submits — no longer works. The cookie doesn’t ride along, the request is unauthenticated, the transfer fails.
Great. But:
Where CSRF comes back
1. Legacy endpoints that also accept GET
Some older APIs accept both GET and POST for the same action. GET /delete-account?id=42 gets hit with SameSite=Lax’s top-level navigation rule — the cookie IS sent. A link on attacker.com pointing to that URL, clicked by the victim, executes the action.
Audit your app: no state-changing operation should respond to GET. Ever.
2. Subdomain user content
If users can host content on *.example.com (blogs, landing pages, user profiles with HTML), those subdomains are “same site” as your main app. SameSite=Lax allows them to POST to your main site, and the cookie rides along. Suddenly CSRF is back, on a subdomain you thought was safe.
Defense: use separate domains for user content (usercontent.example.net), or add a CSRF token for state-changing requests.
3. Embeddable widgets
If your app is embedded as an iframe anywhere (Stripe checkout, a payment widget, a chat support tool), and you’ve set SameSite=None; Secure to make that work, you’re back to classic CSRF territory in those contexts.
Defense: for embedded flows, use a separate token bound to the request.
4. Sec-Fetch-Site check as a cheap fallback
Most modern browsers send a Sec-Fetch-Site header on every request:
same-origin— from the same originsame-site— from the same registrable domaincross-site— from elsewherenone— no referrer (direct navigation)
For state-changing endpoints, you can reject anything that’s not same-origin. It’s a one-header check and catches most real-world CSRF attempts.
The rule
Modern CSRF defense isn’t “SameSite=Lax and forget.” It’s:
- No state change on GET
- Audit your subdomain boundaries
- Add a CSRF token OR
Sec-Fetch-Sitecheck on mutating endpoints
Do all three and you’ve closed the gaps that SameSite=Lax alone doesn’t.