We built an identity server — Bachué — with password login, a second factor, and cookie sessions. The cookies carry SameSite=Lax, and we took for granted what "everyone knows": that SameSite=Lax protects against CSRF.
We didn't trust our own review. The author of a piece of code is the worst placed to audit it: what they reasoned already feels verified. So we asked three different models to attack it, each on its own, with a single brief: find the most serious defect; think like someone trying to break in.
A false alarm, then the real one
The first model shouted "CSRF!" with the wrong mechanics: it believed SameSite=Lax lets a cross-site POST through. It doesn't — Lax won't send the cookie on a POST from another site. A second model refuted it. So far, noise.
The third found the real one, and it's subtle: login-CSRF, which SameSite does not cover.
Why Lax protects logout but not login
SameSite=Lax protects endpoints that need the victim's cookie: logging out, changing settings. A POST from the attacker's site arrives without that cookie, the server sees no session, and nothing happens.
But /login and /register are different: they don't need the victim's cookie. The attacker puts their own credentials in the form, and all the server does is set a new cookie in the response. And a first-party Set-Cookie is honored no matter where the request came from. The result: the victim ends up browsing authenticated as the attacker, without realizing it — and everything they type afterward lives in the attacker's account. SameSite has no effect on this.
Claiming isn't proving
"SameSite covers CSRF" was a claim, and like every security boundary, it seemed to hold until someone crossed it. The proof — three independent adversaries — took minutes and found what a single look, ours included, would have passed.
The fix is one line of discipline: every state-changing POST — including the ones that don't yet require a session — checks that the request is first-party (Sec-Fetch-Site) before processing it. As a rule, not a per-endpoint patch the next endpoint forgets.
The lesson is worth more than the bug: reviewing your own security code isn't enough. Put independent adversaries to prove the boundary holds. If you only claim it, one day it won't — silently, at the spot no one looked at again.
Xiliux