Hoe deze controle werkt
Scanaris kijkt naar het hoofdantwoord van je pagina en controleert twee voorwaarden tegelijk. Ten eerste: het antwoord zet minstens één cookie (Set-Cookie) waarvan de naam begint met sess, sid, auth, token, jwt, connect.sid, phpsessid of asp.net, dus een cookie dat naar alle waarschijnlijkheid een sessie identificeert. Ten tweede: hetzelfde antwoord draagt Cache-Control met de richtlijn public. Alleen als beide gelden, opent hij een bevinding met gemiddelde ernst met de cookienaam en de headerwaarde. Het is bewust streng: een voorkeurencookie met publieke cache, of een sessiecookie zonder public, wordt niet gemeld.
Waarom dit belangrijk is
Cache-Control: public is een expliciete instructie aan elke gedeelde cache (een CDN, een reverse proxy zoals Varnish of nginx, de proxy van een bedrijf of provider) dat dat antwoord mag worden opgeslagen en aan andere gebruikers geserveerd. Bevat datzelfde antwoord een sessie-Set-Cookie, dan kan de cache het cookie samen met de body opslaan en aan de volgende bezoeker geven. Die bezoeker komt binnen met de sessie van de vorige, zonder wachtwoord, en ziet diens gegevens. Het is het soort fout dat je ontdekt wanneer een klant schrijft dat hij "in het account van iemand anders is beland". Het is banken en grote winkels overkomen, altijd door dezelfde combinatie: gepersonaliseerde inhoud gemarkeerd als cachebaar.
Waarom het gebeurt
Meestal door een te brede cacheregel: een add_header Cache-Control "public, max-age=3600" in nginx die op alle routes werkt, ook op die welke sessies aanmaken; een "Cache Everything"-regel in Cloudflare; of een routeRules-regel met cacheheaders voor '/**' in een app die cookies zet op de hoofdpagina. De meeste CDN's weigeren standaard antwoorden met Set-Cookie op te slaan, en nginx ook, tenzij je hem zegt die header te negeren; maar een expliciete public ben jij die hun zegt het wel te doen.
Hoe je het oplost
- Antwoorden die een sessie aanmaken of vernieuwen dragen
Cache-Control: private, no-store. Dat doenexpress-session, Laravel, Django en Rails standaard; zie jepublic, dan heeft een latere laag het toegevoegd. - Scheid cacheregels per routetype.
publicmet een langemax-agevoor statische bestanden met een hash in de naam (/_nuxt/,/_next/static/,/assets/);privateofno-storevoor HTML die van de gebruiker afhangt. In nginx gebruik je apartelocation-blokken; in Nuxt en Next.js regels per route in plaats van één globale. - Zet het sessiecookie niet op openbare cachebare pagina's. Heeft de homepage geen sessie nodig, dan hoort hij er geen aan te maken: maak het cookie aan bij het inloggen of bij het betreden van het dashboard.
- Gebruik je een CDN met "Cache Everything" of iets dergelijks, voeg dan een regel toe om antwoorden met
Set-Cookieen geauthenticeerde routes niet te cachen. En voegVary: Cookietoe als extra vangnet, al vervangt dat het bovenstaande niet.
Controleer of het opgelost is
curl -sI https://jouwdomein.nl | grep -iE "^(set-cookie|cache-control|vary):"
curl -sI https://jouwdomein.nl/dashboard | grep -iE "^(set-cookie|cache-control):"
Is er een Set-Cookie met een sessienaam, dan moet de Cache-Control van dat antwoord private, no-store of no-cache zijn, nooit public. Doe het tweede verzoek met en zonder je sessiecookie: het geauthenticeerde antwoord mag zonder cookie nooit verschijnen.
Veelgestelde vragen
Mijn CDN zegt dat het antwoorden met cookies niet cachet. Dus een vals positief?
Het lek gebeurt vandaag waarschijnlijk niet, maar je hangt af van een standaardgedrag dat een nieuwe regel of een wissel van aanbieder kan uitschakelen. De header zegt dat het publiek is; herstel de header en stop met vertrouwen op de uitzondering.
En analytics- of toestemmingscookies?
Die worden niet gemeld: alleen namen die op een sessie wijzen tellen. Een toestemmingscookie op een openbare gecachte pagina is normaal. Hoe sessiecookies geconfigureerd moeten zijn (Secure, HttpOnly, SameSite), bekijkt onveilige cookies.
Mijn sessie is een token in localStorage, geen cookie.
Dan geldt deze controle niet, maar token in opslag wel: een token dat vanuit JavaScript bereikbaar is, heeft andere problemen.