Hoe cookies en sessies je ingelogd houden
Je logt één keer in en daarna weet een website op elke volgende pagina wie je bent. Dat is minder vanzelfsprekend dan het lijkt, want HTTP onthoudt zelf niets. Elke vraag van je browser staat op zichzelf. Dat je toch ingelogd blijft, komt door cookies: een paar regels tekst die de server je browser laat bewaren en die de browser daarna bij elke vraag terugstuurt.
In dit artikel volg ik dat heen-en-weer met de echte cookies van de inlogpagina van GitHub en een paar andere grote sites. Je ziet wat een sessiecookie is, waarom het meestal alleen een willekeurig nummer bevat, en wat de vlaggen Secure, HttpOnly en SameSite regelen. Aan het eind komt de wet aan bod die de cookiebanner verplicht. De technische regels komen uit RFC 6265 (april 2011) en de opvolger daarvan, die als Internet-Draft bij de IETF ligt (versie 22, december 2025).
HTTP vergeet alles
Vraag je twee pagina's op, dan zijn dat voor de server twee losse gebeurtenissen. RFC 6265 omschrijft cookies daarom als de manier waarop servers een sessie met toestand kunnen bijhouden over het "grotendeels toestandsloze" HTTP. Dezelfde RFC is opvallend eerlijk over de kwaliteit: cookies hebben volgens de tekst "veel historische onhebbelijkheden" die beveiliging en privacy schaden, maar ze worden overal gebruikt.
Set-Cookie en Cookie: het heen-en-weer
Het mechanisme bestaat uit twee headers. De server stuurt Set-Cookie in een antwoord, de browser stuurt Cookie in elke volgende vraag. Vanaf een Nederlandse verbinding kreeg ik bij https://github.com/login deze drie terug (waarden ingekort):
set-cookie: _gh_sess=...; path=/; HttpOnly; secure; SameSite=Lax
set-cookie: _octo=GH1.1....; expires=Thu, 28 Oct 2027 04:30:34 GMT; domain=.github.com; path=/; secure; SameSite=Lax
set-cookie: logged_in=no; expires=Thu, 28 Oct 2027 04:30:34 GMT; domain=.github.com; path=/; HttpOnly; secure; SameSite=Lax
Bij de volgende vraag ging alles terug in één regel, zonder de attributen erachter:
cookie: logged_in=no; _gh_sess=dH2VwOhYyGaY...; _octo=GH1.1.172138...
De server krijgt dus alleen naam en waarde te zien. Wanneer een cookie verloopt en naar welke adressen hij mee mag, is iets wat de browser bijhoudt en zelf afdwingt.
Een sessie is een nummer in een kast
De waarde van _gh_sess was 350 tekens lang. Wat erin zit, is van buitenaf niet te zien. RFC 6265 beschrijft in sectie 8.4 het gebruikelijke patroon: zet niet de sessiegegevens zelf in een cookie, maar een session identifier. Dat is een willekeurige waarde die de server gebruikt als sleutel om de echte gegevens op te zoeken in zijn eigen opslag. Daar kan bijvoorbeeld staan dat deze sessie bij jouw account hoort en wat er in je winkelwagen ligt.
Het voordeel volgens de RFC: wie zo'n cookie onderschept, heeft alleen iets dat werkt bij die ene server, geen gevoelige inhoud. Maar met die waarde kan hij wel als jou optreden. Een sessie-ID moet daarom onvoorspelbaar zijn. In Python maakt secrets.token_urlsafe(32) uit 32 willekeurige bytes (256 bits) een tekst van 43 tekens. Raden is dan geen optie.
Na het inloggen koppelt de server jouw account aan zo'n sessie. Daarna bewijst je browser bij elke vraag met dat ene cookie dat jij het bent.
Hoe lang een cookie blijft
Een cookie zonder Expires of Max-Age is een sessiecookie. De browser bewaart hem tot "de huidige sessie voorbij is", en RFC 6265 laat de browser zelf bepalen wat dat betekent. _gh_sess heeft geen verloopdatum. In het cookiebestand dat curl bijhoudt, staat daarom een 0 in de kolom voor de vervaltijd.
Met Expires (een datum) of Max-Age (een aantal seconden) wordt een cookie blijvend. Staan ze er allebei, dan wint Max-Age. De concepttekst voegt een bovengrens toe: browsers moeten een levensduur inkorten tot hooguit 400 dagen. Google zette bij mijn bezoek op 28 oktober 2026 een cookie SOCS met verloopdatum 27 november 2027. Dat is 395 dagen, net onder die grens.
Naar wie gaat hij mee: Domain en Path
Bij GitHub hebben _octo en logged_in hebben domain=.github.com, _gh_sess heeft geen Domain. Met een Domain-attribuut gaat een cookie mee naar dat domein en alle subdomeinen; de punt aan het begin wordt volgens de RFC genegeerd. Zonder Domain gaat hij alleen terug naar precies de host die hem zette. Het sessiecookie gaat dus wel naar github.com maar niet naar een subdomein daarvan.
Path beperkt een cookie tot een deel van de site. De RFC waarschuwt wel dat je daar niet op moet bouwen voor beveiliging.
Secure en HttpOnly
Secure betekent dat de browser het cookie alleen over een beveiligd kanaal meestuurt, in de praktijk HTTPS. Hoe die versleuteling tot stand komt, staat in Wat er gebeurt vóór de eerste byte. De RFC voegt eraan toe dat Secure alleen de vertrouwelijkheid beschermt: een aanvaller kan via een onversleutelde verbinding zo'n cookie nog wel overschrijven.
HttpOnly verbergt een cookie voor JavaScript op de pagina. Komt er via een lek toch kwaadaardig script op een site, dan kan dat script _gh_sess niet uitlezen en wegsturen. _octo heeft geen HttpOnly en is dus wel voor scripts zichtbaar.
Een nieuwere truc zit in de naam. Google stuurde een cookie __Secure-ENID. Volgens de concepttekst accepteert een browser een cookie met het voorvoegsel __Secure- alleen als Secure erbij staat. Bij __Host- is het strenger: ook Path=/ en geen Domain, zodat het cookie aan één host vastzit.
SameSite tegen meeliftende verzoeken
Een browser stuurt cookies mee, wie de vraag ook veroorzaakt. RFC 6265 noemt dat ambient authority. Stel dat een kwaadwillende site een verborgen formulier bevat dat een POST doet naar bank.example/overmaken. Je browser stuurt je bankcookie mee, de bank ziet een geldige sessie en voert het uit. Dat heet cross-site request forgery (CSRF).
SameSite beperkt dat. Een "site" is hier het schema plus het geregistreerde domein, zoals https en github.com. Er zijn drie waarden:
| Waarde | Gaat mee bij |
|---|---|
Strict | alleen vragen vanaf dezelfde site |
Lax | dezelfde site, plus navigatie vanaf een andere site naar een pagina (zoals een klik op een link), alleen met een veilige methode als GET |
None | alles, maar alleen als Secure er ook bij staat |
Met Lax gaat een cookie wel mee als je via een zoekmachine op een site komt, maar dat verborgen POST-formulier krijgt je cookie niet. Ontbreekt SameSite, dan behandelt de browser het cookie volgens de concepttekst in principe als Lax. De tekst noemt Lax zelf wel "verdediging in de diepte" en geen volledige bescherming, omdat een aanvaller bijvoorbeeld een nieuw venster kan openen.
Bij login.microsoftonline.com stonden alle vier de cookies die ik kreeg op SameSite=None, steeds met secure. Dat is de ruimste instelling: deze cookies gaan ook mee als een andere site de vraag veroorzaakt.
Waarom de cookiebanner bestaat
De banner komt niet uit een RFC maar uit de wet. Artikel 11.7a van de Telecommunicatiewet staat het opslaan van of lezen van informatie op het apparaat van een gebruiker alleen toe na duidelijke informatie en toestemming.
Lid 3 maakt een uitzondering voor opslag die strikt noodzakelijk is voor de dienst waar de gebruiker om vraagt. Een sessiecookie om ingelogd te blijven valt daaronder. Ook meten hoe een site presteert mag zonder toestemming, als dat geen of geringe gevolgen heeft voor de privacy. Voor zulke cookies vraagt dit artikel dus geen toestemming. De voorpagina en een artikelpagina van wallieweb.nl stuurden in mijn meting overigens geen enkele Set-Cookie-header mee.
Zelf proberen
De cookies van een site bekijken:
curl -4 -s -o /dev/null -D - https://github.com/login | grep -i set-cookie
Cookies bewaren in een bestand en daarna terugsturen, zoals een browser doet:
curl -4 -s -o /dev/null -c jar.txt https://github.com/login
curl -4 -s -o /dev/null -v -b jar.txt https://github.com/login 2>&1 | grep -i '^> cookie'
> cookie: logged_in=no; _gh_sess=dH2VwOhYyGaY...; _octo=GH1.1.172138...
In jar.txt zie je per cookie het domein, de vervaltijd (0 voor een sessiecookie) en #HttpOnly_ voor de naam als die vlag aan staat.
Een eigen cookie zetten en terugkrijgen bij httpbin.org:
curl -4 -s -D - -o /dev/null "https://httpbin.org/cookies/set?thema=donker" | grep -i set-cookie
curl -4 -s -b "thema=donker" https://httpbin.org/cookies
set-cookie: thema=donker; Path=/
{
"cookies": {
"thema": "donker"
}
}
Tot slot
Wat me opviel, is hoe weinig de server eigenlijk terugkrijgt. Alle regels over levensduur, domeinen en SameSite staan in de Set-Cookie-header, maar de Cookie-header bevat alleen namen en waarden. De hele beveiliging rust dus op de browser die zich aan de afspraken houdt. Verder had ik niet verwacht dat de standaard die dit allemaal regelt, nog steeds RFC 6265 uit 2011 is. SameSite, de 400-dagengrens en de naamvoorvoegsels staan in een opvolger die officieel nog een concept is.