The Session Cookie Without SameSite That Rode Along Cross Site
A forged form on an attacker's page submitted a request to the real site, and the browser attached the victim's session cookie, because a cookie with no…
The incident was a transfer the user never made, initiated from a page on another domain, and the server processed it as authentic, because the request carried the user's session cookie, which the browser attached automatically, correctly, per its defaults, to a cross site request the user never intended. The vulnerability had no malware and no stolen credential. It was the cookie system working as specified, for a specification whose default was written when cross site riding was not yet the attack, and the missing piece was one attribute the cookie never had.
This is the anatomy of CSRF via a cookie without SameSite, and of why the attribute, small as it is, is a boundary, and why its absence is a vulnerability with the browser as the accomplice.
This was a session cookie set without SameSite, on a site whose state changing endpoints accepted the session as the only authority, and the attack was a form post from a third party page, the oldest shape in the book.
The ride along, mechanically
The browser attaches cookies to requests by matching the cookie's domain to the request's host, and historically it did not matter where the request originated, so a form on an attacker's site that posts to the victim site carries the victim's session cookie, because the cookie's contract is with the destination host, not with the page that launched the request. The SameSite attribute is the contract amendment: it tells the browser to withhold the cookie when the request's site differs from the cookie's site, converting the ride along into a refused attachment, and the forged request arrives anonymous and is rejected.
Without the attribute, the legacy default applies, and the legacy default, after a period of browsers moving to Lax by default, still leaves older cookies and explicitly unset ones in the historical position, and crucially, Lax itself withholds the cookie on cross site form posts but permits it on top level navigations, so a state changing GET, the endpoint that mutates on a link, is still rideable under Lax, which is why the attribute's value and the endpoint's method both matter.
Why the server side alone does not save it
The classical defence, a per session token the form must echo, works, and remains necessary, because it makes the request unforgeable regardless of the cookie. But the token is a check the server must remember to add to every state changing endpoint, and the missing token on one endpoint is the vulnerability, which is a per endpoint discipline, and per endpoint disciplines have holes, per reviewing input validation at boundaries, and the common root is the property, not the platform.
SameSite is different in kind: it is a property of the cookie, set once, that withholds the credential from the cross site request before it is sent, so the forged request never carries the authority, which is removal rather than checking, and removal at the browser is the only defence that covers every endpoint at once, including the one the token forgot.
The two are complementary layers, the attribute shrinking the surface and the token verifying intent, and the incident's root cause was that the site had neither on the affected endpoint, but the attribute's absence was the enabler, because with it the attack class dies browser side.
The values, and what each buys
Strict withholds the cookie on all cross site usage, including navigation, which is the strongest and breaks flows that rely on the cookie arriving from a link, such as some payment returns, so it is chosen deliberately where the session is sensitive and the flows are same site.
Lax, the sane default for most sessions, withholds on cross site subrequests, the form post and the image and the fetch, killing the classic CSRF ride, while permitting the top level navigation, keeping the link based login flow working, at the cost of leaving state changing GETs exposed, which is an argument against state changing GETs, not against Lax.
None explicitly restores the ride along, and requires Secure, and is the value the attacker wants you to set, so its presence in the codebase is a review event, justified only by a genuine cross site embedding need, and logged as the deliberate opening of the boundary it is.
The audit and the test
The audit is one grep over the cookie setting code: every session cookie must name a SameSite value, and an unset one is the legacy default, which is the vulnerability's raw material, and the review treats the absent attribute as the absent Secure was treated in the secure cookie http login redirect loop, a missing boundary named by its absence.
The test is the forged form: a page on another origin submits the state change, and the assertion is that the request, carrying the cookie as the browser will, is rejected, run against the real cookie policy, because the test that stubs the cookie tests the token and not the attribute, and the attribute is the layer under test. The test under Lax must use a form post, the ride the attribute blocks, and a separate test asserts the state changing GET, if any survive, are gone, because they are the Lax residual.
What I now do
A cookie without SameSite rides along on cross site requests by default, attaching the victim's session to the attacker's forged request, so the browser, working correctly, becomes the delivery mechanism. Set the attribute on every session cookie, Lax as the floor and Strict where the session is sensitive, keep the token as the second layer, and treat None as a logged, deliberate opening.
The transfer the user never made was authorised by a cookie doing exactly what it was told. The lesson I keep is the property, not the incident.