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

Chrome stuurt een cookie zonder expliciete SameSite-attribuut de eerste 120 seconden na het zetten ervan alsnog mee bij een cross-site POST-request. Dat is een bewuste uitzondering op de eigen Lax-default, ingebouwd om SSO-flows niet te breken. Hoe die uitzondering werkt, hoe het venster langer openblijft dan "net na het inloggen", en hoe u zich er daadwerkelijk tegen wapent.

SameSite=Lax in één zin

Sinds Chrome 80 (februari 2020) krijgt elke cookie zonder expliciete SameSite-attribuut automatisch SameSite=Lax. Het idee: een cookie wordt alleen meegestuurd bij requests vanuit uw eigen site, plus bij "veilige" top-level navigatie vanaf een andere site, zoals een gebruiker die op een link klikt. Bij cross-site POST-requests wordt de cookie niet meegestuurd: dat is precies de klassieke CSRF-vector, want een auto-submittend formulier op een aanvallerssite is per definitie cross-site. Daarmee elimineert SameSite=Lax in theorie het grootste deel van CSRF, zonder dat een ontwikkelaar er iets voor moet doen.

Dat is de theorie. In de praktijk bouwde Chrome zelf een uitzondering in die dit verhaal een stuk genuanceerder maakt.

De uitzondering die niemand leest: Lax+POST

Uit de officiële Chromium-documentatie: een cookie die hoogstens 2 minuten oud is, wordt wél meegestuurd bij een top-level cross-site POST-request, ook als die cookie geen expliciete SameSite-attribuut heeft. Chromium noemt dit zelf "Lax+POST" of "Lax+Unsafe", en is er open over dat het een bewuste, tijdelijke mitigatie is, geen bug.

De reden is pragmatisch: sommige Single Sign-On-implementaties ronden een login af met een cross-site POST-redirect terug naar de site, vlak na het zetten van de sessiecookie. Forceer je daar strikte Lax-regels op af, dan breekt die loginflow. Chrome koos voor een genadeperiode van 120 seconden in plaats van die flows in één keer te breken.

Een nuance die vaak over het hoofd wordt gezien, en de belangrijkste van dit hele artikel: deze uitzondering geldt alleen voor cookies die geen SameSite-attribuut specificeren. Schrijft u expliciet SameSite=Lax in de Set-Cookie-header, dan krijgt u de "normale" Lax-regels, zonder de 2-minuten-coulance. 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 voor een beperkte tijd weer open. Het aanvalsrecept is simpel:

  • Het doelwit gebruikt een sessiecookie zonder expliciete SameSite-attribuut (de default, en nog steeds zeer gebruikelijk).
  • Er is een state-changing endpoint bereikbaar via POST zonder anti-CSRF-token, bijvoorbeeld "e-mailadres wijzigen" of "wachtwoord resetten".
  • Het slachtoffer heeft die cookie minder dan 2 minuten geleden gekregen.

Bij punt drie denken de meeste mensen: dan moet de aanvaller toch precies binnen 2 minuten na een login toeslaan, en dat is onpraktisch. Dat klopt, zolang de aanvaller op toeval moet wachten. Maar wachten op toeval is niet de enige optie.

De truc die het venster oprekt: cookie refresh

De 2 minuten worden niet geteld vanaf de allereerste keer dat die cookienaam ooit gezet is. Ze worden geteld vanaf het laatste moment waarop de server een Set-Cookie voor die cookie verstuurde. Stuurt de server een nieuwe sessiecookie elke keer dat een gebruiker opnieuw inlogt, ook als die al ingelogd was, dan kan een aanvaller dat moment forceren en zo zelf een vers 2-minuten-venster openen, precies op het moment dat hem uitkomt.

Dit is geen hypothese: PortSwigger's Web Security Academy heeft er een eigen lab voor, "SameSite Lax bypass via cookie refresh". Het scenario: een site met OAuth-login zet bij elke voltooiing van de OAuth-flow een nieuwe sessiecookie, ook als de gebruiker al een geldige sessie had. De aanvaller stuurt de browser van het slachtoffer via een onzichtbare top-level navigatie, bijvoorbeeld een auto-redirecterende pop-up of window.open, langs /social-login. Dat rondt de OAuth-flow in stilte af, de server zet een verse cookie, en de klok van de 2 minuten begint opnieuw te lopen voor een sessie die al lang bestond. Een paar seconden later submit een verborgen formulier de daadwerkelijke CSRF-payload, bijvoorbeeld een wijziging van het e-mailadres, en de verse cookie gaat gewoon mee.

Het resultaat: het venster is niet "de eerste 2 minuten na login", maar "de eerste 2 minuten na elke gelegenheid waarop de aanvaller een herhaalde cookie-uitgifte kan afdwingen". Bij sites die de sessiecookie bij elke herauthenticatie of zelfs bij elke request verversen, de zogeheten rolling of sliding sessions, kan dat venster voor een actief ingelogde gebruiker vrijwel continu openstaan.

Dit is geen 0-day

Niets hiervan is een onbekende kwetsbaarheid in Chrome. Het staat met naam en toelichting in de officiële Chromium-documentatie, en PortSwigger onderwijst de cookie-refresh-techniek al jaren in de Web Security Academy. Het probleem is niet dat dit verborgen is, maar dat veel ontwikkelaars SameSite=Lax (impliciet, via de browser-default) verwarren met "cross-site POST is altijd geblokkeerd."

Hoe u dit in uw eigen applicatie dichtzet

Zet SameSite altijd expliciet

De eenvoudigste, meest concrete fix: schrijf SameSite=Lax (of Strict, waar mogelijk) altijd zelf in de Set-Cookie-header, in plaats van te vertrouwen op de browser-default. Zoals hierboven beschreven geldt de Lax+POST-coulance alleen voor cookies zonder expliciete attribuut. Eén woord verschil in uw response-header, en het hele aanvalsscenario in dit artikel is niet meer van toepassing.

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, gratis verdedigingslaag, maar wel een laag: geen vervanging voor anti-CSRF-tokens op state-changing endpoints. Voor gevoelige acties zoals e-mail wijzigen, wachtwoord resetten, betalingen of accountinstellingen blijft een unpredictable token of een double-submit-cookiepatroon de daadwerkelijke garantie. SameSite vangt wat er doorheen glipt, maar vervangt de check zelf niet.

Audit endpoints die stilletjes een sessiecookie vernieuwen

Loop langs elk endpoint dat een Set-Cookie voor uw sessie verstuurt zonder dat de gebruiker bewust opnieuw inlogt: SSO-callbacks, "remember me"-vernieuwing, rolling-session-middleware die de cookie bij elke request opnieuw zet. Is een van die endpoints bereikbaar via een simpele top-level navigatie, zoals een link, een redirect of een window.open, zonder dat de gebruiker iets hoeft te doen? Dan is dat precies het gadget dat de cookie-refresh-techniek nodig heeft.

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

Heeft uw eigen inlogflow een cross-site POST-redirect nodig die de sessie moet overleven? Gebruik dan bewust SameSite=None; Secure voor die specifieke cookie, in plaats van te steunen op een coulanceregel die Chrome zelf al jaren als "tijdelijk" bestempelt en op enig moment kan schrappen.

Conclusie

SameSite=Lax is een goede default die een groot deel van CSRF wegneemt zonder dat iemand er iets voor moest doen. Maar "een groot deel" is niet "alles", en al helemaal niet "vanaf seconde 1". De Lax+POST-mitigatie is een bewuste, gedocumenteerde uitzondering, en de cookie-refresh-techniek laat zien dat het venster dat ze opent vaak groter is dan "de eerste 2 minuten na inloggen".

De fix kost in de meeste stacks één regel: schrijf SameSite expliciet in plaats van de browser-default te laten gelden, en behandel het als aanvulling op anti-CSRF-tokens, niet als vervanging.

Vertrouwt uw applicatie stilletjes op browser-defaults?

SameSite, CORS, cookie-vlaggen: precies het soort configuratie dat in een handmatige pentest wordt nagelopen en in een geautomatiseerde scan wordt overgeslagen.

Naar webapplicatie-pentest →