De 2 minuten waarin SameSite=Lax geen CSRF-bescherming biedt

Een cookie zonder expliciete SameSite-attribuut wordt de eerste 120 seconden na het zetten gewoon meegestuurd bij een cross-site POST-request. Chrome doet dat met opzet, als uitzondering op zijn eigen Lax-default, om SSO-flows niet te breken. Hieronder leest u hoe die uitzondering werkt, waarom het venster vaak langer openstaat dan alleen "net na het inloggen", en wat u er zelf tegen doet.

SameSite=Lax in één zin

Sinds Chrome 80 (februari 2020) krijgt elke cookie zonder expliciete SameSite-attribuut automatisch SameSite=Lax mee. Zo'n cookie gaat dan alleen nog mee met requests vanaf uw eigen site, plus met "veilige" top-level navigatie vanaf een andere site, zoals een gebruiker die op een link klikt. Bij een cross-site POST blijft de cookie thuis, en dat is geen toeval. Een auto-submittend formulier op een aanvallerssite is per definitie cross-site, en dus de klassieke CSRF-vector. Zo neemt SameSite=Lax in theorie het grootste deel van CSRF weg, zonder dat een ontwikkelaar er iets voor hoeft te doen.

Tenminste, in theorie. In de praktijk bouwde Chrome zelf een uitzondering in die dit verhaal een stuk minder zwart-wit maakt.

De uitzondering die niemand leest: Lax+POST

De officiële Chromium-documentatie is er duidelijk over. Een cookie die hoogstens 2 minuten oud is, gaat wél mee bij een top-level cross-site POST-request, ook zonder expliciete SameSite-attribuut. Chromium noemt dit zelf "Lax+POST" of "Lax+Unsafe", en zegt er meteen bij dat het om een bewuste, tijdelijke mitigatie gaat. Geen bug dus.

De reden is praktisch. Sommige Single Sign-On-implementaties ronden een login af met een cross-site POST-redirect terug naar de site, meteen nadat de sessiecookie is gezet. Wie daar strikte Lax-regels op loslaat, breekt precies die loginflow. Chrome gaf daarom 120 seconden coulance, in plaats van al die flows in één klap onderuit te halen.

En dan het detail dat het vaakst wordt gemist, en meteen het belangrijkste van dit hele artikel. Deze uitzondering geldt alleen voor cookies die geen SameSite-attribuut meegeven. Zet u zelf SameSite=Lax in de Set-Cookie-header, dan gelden de gewone Lax-regels en is de 2-minuten-coulance volledig van tafel. Daar komen we bij de oplossing op terug.

Waarom dit een achterdeur is voor CSRF

Het hele punt van SameSite=Lax als CSRF-mitigatie is dat een cross-site POST de cookie niet meekrijgt. Lax+POST zet die deur tijdelijk weer op een kier. Het aanvalsrecept is kort:

  • Het doelwit gebruikt een sessiecookie zonder expliciete SameSite-attribuut. Dat is de default, en nog altijd heel gewoon.
  • Er is een state-changing endpoint dat via POST bereikbaar is zonder anti-CSRF-token. Denk aan "e-mailadres wijzigen" of "wachtwoord resetten".
  • Het slachtoffer kreeg die cookie minder dan 2 minuten geleden.

Punt drie lijkt de hele aanval onpraktisch te maken. De aanvaller zou binnen 2 minuten na een login moeten toeslaan, en dat is gokken op timing. Dat klopt ook, zolang hij op toeval moet wachten. Alleen is wachten op toeval niet de enige optie.

De truc die het venster oprekt: cookie refresh

Die 2 minuten lopen niet vanaf het allereerste moment dat de cookie ooit gezet werd. Ze lopen vanaf de laatste keer dat de server een Set-Cookie voor die cookie stuurde. En deelt de server bij elke nieuwe login een verse sessiecookie uit, ook aan iemand die al was ingelogd, dan kan een aanvaller dat moment zelf uitlokken. Hij opent zo een vers venster van 2 minuten, precies wanneer het hem uitkomt.

En dit is geen theorie. PortSwigger's Web Security Academy heeft er een heel lab voor, "SameSite Lax bypass via cookie refresh". Het werkt zo. Een site met OAuth-login zet bij elke voltooide OAuth-flow een nieuwe sessiecookie, ook als de gebruiker allang een geldige sessie had. De aanvaller stuurt de browser van het slachtoffer via een onopvallende top-level navigatie langs /social-login, bijvoorbeeld met een auto-redirecterende pop-up of window.open. Die ronde maakt de OAuth-flow stilletjes af, de server zet een verse cookie, en de teller van 2 minuten begint opnieuw, voor een sessie die er allang was. Een paar seconden later verstuurt een verborgen formulier de echte CSRF-payload, bijvoorbeeld een gewijzigd e-mailadres, en de verse cookie reist gewoon mee.

Daarmee verschuift het hele plaatje. Het kwetsbare venster is niet langer "de eerste 2 minuten na de login", maar "de eerste 2 minuten na elk moment waarop de aanvaller een nieuwe cookie-uitgifte kan afdwingen". En bij sites die de sessiecookie bij elke herauthenticatie of zelfs bij elke request verversen, de zogeheten rolling of sliding sessions, staat dat venster voor een actief ingelogde gebruiker bijna onafgebroken open.

Dit is geen 0-day

Niets hiervan is een onbekende kwetsbaarheid in Chrome. Het staat met naam en uitleg in de officiële Chromium-documentatie, en PortSwigger behandelt de cookie-refresh-techniek al jaren in de Web Security Academy. Het zit 'm dus niet in geheimhouding, maar erin dat veel ontwikkelaars impliciete SameSite=Lax (via de browser-default) gelijkstellen aan "een cross-site POST wordt altijd geblokkeerd."

Zit dit soort subtiele CSRF-gaten ook in uw applicatie? Bij een handmatige penetratietest zoeken wij juist naar dit type logicafouten die geautomatiseerde scanners missen.

Plan een gratis intake →

Hoe u dit in uw eigen applicatie dichtzet

Zet SameSite altijd expliciet

De simpelste fix is meteen de meest concrete. Schrijf SameSite=Lax (of Strict, waar dat kan) altijd zelf in de Set-Cookie-header en laat het niet aan de browser-default over. De Lax+POST-coulance geldt immers alleen voor cookies zonder expliciete attribuut. Eén woord extra in uw response-header, en het hele aanvalsscenario uit dit artikel gaat niet meer op.

Set-Cookie: session=abc123; Path=/; HttpOnly; Secure
   (impliciet Lax, kwetsbaar voor het 2-minuten-venster)

Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
   (expliciet, geen Lax+POST-uitzondering)

Vertrouw niet op SameSite als enige CSRF-verdediging

SameSite is een sterke verdedigingslaag, en gratis, maar het blijft een láág. Geen vervanging voor anti-CSRF-tokens op state-changing endpoints. Voor gevoelige acties als e-mail wijzigen, wachtwoord resetten, betalingen of accountinstellingen blijft een onvoorspelbaar token of een double-submit-cookiepatroon de echte garantie. SameSite vangt op wat erdoorheen glipt, maar neemt die controle niet over.

Audit endpoints die stilletjes een sessiecookie vernieuwen

Loop elk endpoint na dat een Set-Cookie voor uw sessie verstuurt zonder dat de gebruiker er bewust voor inlogt. Denk aan SSO-callbacks, "remember me"-vernieuwing of rolling-session-middleware die de cookie bij elke request opnieuw zet. Valt zo'n endpoint te bereiken via een doodgewone top-level navigatie, een link, een redirect of een window.open, zonder dat de gebruiker iets hoeft te doen? Dan heeft u het gadget gevonden waar de cookie-refresh-techniek om vraagt.

Voor SSO/OAuth-flows: kies expliciet, vertrouw niet op coulance

Heeft uw eigen inlogflow een cross-site POST-redirect nodig waar de sessie doorheen moet? Kies dan bewust voor SameSite=None; Secure op die ene cookie, in plaats van te leunen op een coulanceregel die Chrome zelf al jaren "tijdelijk" noemt en elk moment kan schrappen.

Conclusie

SameSite=Lax is een goede default. Hij haalt een groot deel van CSRF weg zonder dat iemand er iets voor hoefde te doen. Maar "een groot deel" is niet "alles", en zeker niet "vanaf seconde één". Lax+POST is een bewuste, gedocumenteerde uitzondering, en de cookie-refresh-techniek laat zien dat het venster dat ze openzet vaak groter is dan "de eerste 2 minuten na het inloggen".

In de meeste stacks kost de fix één regel. Schrijf SameSite expliciet in plaats van de browser-default zijn gang te laten gaan, en zie het als aanvulling op anti-CSRF-tokens, niet als vervanging.

Vertrouwt uw applicatie stilletjes op browser-defaults?

SameSite, CORS, cookie-vlaggen. Het soort configuratie dat een handmatige pentest naloopt en een geautomatiseerde scan domweg overslaat.

Naar webapplicatie-pentest →