Hoe deze controle werkt
Scanaris leest het JavaScript van je homepage, inclusief de scripts die hij vanaf je domein laadt, en zoekt aanroepen van localStorage.setItem of sessionStorage.setItem waarvan de sleutel op inloggegevens wijst: token, jwt, auth, secret, password, api_key, access, refresh of bearer. Vindt het er een, dan opent het een bevinding met lage ernst. Laag, omdat een token in de browser bewaren op zichzelf geen inbraak is; het wordt er een in combinatie met welke scriptinjectie dan ook, en daarom is het de moeite waard om te weten.
Waarom het ertoe doet
Alles in localStorage kan gelezen worden door elke code die op je pagina draait: je eigen scripts, de scripts van derden die je laadt (analytics, chats, widgets) en elk script dat een aanvaller weet te injecteren. Eén XSS-kwetsbaarheid, in je code of in een afhankelijkheid, is genoeg om localStorage.getItem('token') naar een vreemde server te laten reizen en iemand de sessie van je gebruiker van elders te laten gebruiken, zonder wachtwoord en zonder dat hij het merkt. Een cookie met HttpOnly is onzichtbaar voor JavaScript, dus diezelfde XSS kan hem niet stelen; hij kan verzoeken doen zolang de pagina open is, wat erg genoeg is, maar hij kan de sessie niet meenemen.
Waar het meestal vandaan komt
Van de standaardconfiguratie van veel authenticatiebibliotheken in single-page-applicaties. De Supabase-client voor de browser bewaart de sessie in localStorage tenzij je anders zegt, net als veel "zelfgemaakte" JWT-voorbeelden met Express. Lovable, Bolt en v0 genereren dat patroon omdat het in de snelstartdocumentatie staat.
Hoe je het oplost
- Sessie in HttpOnly-cookies. Heb je een server (Next.js, Nuxt, Express, een Edge Function), laat die dan het token van de provider ontvangen en in een cookie met
HttpOnly,SecureenSameSite=Laxbewaren. De browser stuurt hem vanzelf mee en ziet hem nooit. - Met Supabase: gebruik in Next.js of Nuxt
@supabase/ssr, dat de sessie in cookies bewaart; in een pure SPA zonder server accepteer je het risico en compenseer je het met een strikte CSP, want er is geen andere plek om hem te bewaren. - Korte tokens en veilige verversing. Moet het toegangstoken in het geheugen of in storage leven, laat het dan in minuten verlopen en zet het refresh-token in een
HttpOnly-cookie. - Geheugen in plaats van storage. Het token in een JavaScript-variabele (niet in storage) bewaren beschermt het tegen scripts die storage lezen, maar niet tegen een actieve XSS op dezelfde pagina. Beter dan localStorage, slechter dan een cookie.
- Verklein het XSS-oppervlak. Een CSP zonder
'unsafe-inline'inscript-src, alle gebruikers-HTML opschonen en de scripts van derden die je laadt nakijken. Dat is de verdediging die het token in storage onbelangrijk maakt.
Controleer of het is opgelost
curl -s https://jouwdomein.com | grep -oE 'src="[^"]+\.js[^"]*"' | cut -d'"' -f2 | head -3
curl -s https://jouwdomein.com/assets/index-XXXX.js | grep -oE "(local|session)Storage\.setItem\([^)]{0,60}" | head
Geeft het tweede commando regels met token, auth of iets dergelijks in de sleutel, dan is het patroon er nog. In de browsertools, tabblad Toepassing, zie je wat je app na het inloggen in storage bewaart.
Veelgestelde vragen
Supabase bewaart de sessie standaard in localStorage. Is dat fout?
Het is het compromis dat een SPA zonder server sluit. Heb je een server, stap dan over op @supabase/ssr. Zo niet, let dan op de CSP en de scripts van derden: dat token is zo veilig als de pagina eromheen.
Is sessionStorage veiliger dan localStorage?
Alleen in duur: hij wordt gewist bij het sluiten van het tabblad. Elk script op de pagina leest hem net zo goed.
Waarom lage ernst als het de sessie kan stelen?
Omdat er een andere kwetsbaarheid (een XSS) nodig is om het te misbruiken. De controle waarschuwt je zodat je met kennis van zaken beslist; heeft je site al een strikte CSP en toont hij geen niet-opgeschoonde gebruikersinhoud, dan is het echte risico klein.