JWT blijft geldig na uitloggen
CWE-613CWE-384OWASP A07:2021Bijgewerkt 3 september 20265 min leestijd
Een JSON Web Token wordt gecontroleerd op zijn handtekening en niet opgezocht in een database. Daardoor blijft hij geldig tot de vervaldatum, ook nadat de gebruiker heeft uitgelogd, zijn wachtwoord heeft gewijzigd of is geblokkeerd. Met korte geldigheidsduren, refresh-tokens en een intrekkingslijst herstelt u de controle.
JSON Web Tokens danken hun populariteit aan één eigenschap: de server hoeft niets op te zoeken. De handtekening klopt of niet, en dat is genoeg. Diezelfde eigenschap maakt het lastig om een token vóór zijn vervaldatum ongeldig te verklaren, wat een probleem is op precies de momenten waarop dat het hardst nodig is. Er is een nette manier om dat op te lossen.
Waarom is een JWT lastig in te trekken?
Bij klassiek sessiebeheer bewaart de server een lijst van geldige sessies. Uitloggen betekent dan: streep die regel door. Bij een JSON Web Token bestaat die lijst niet. Het token draagt de gegevens zelf mee (wie de gebruiker is, welke rechten hij heeft en tot wanneer het geldig is) en is ondertekend met een sleutel van de server. Bij elk verzoek controleert de server alleen die handtekening en de vervaldatum.
Dat is een bewuste ontwerpkeuze met een duidelijk voordeel: er is geen gedeelde sessieopslag nodig, wat schaalbaarheid vereenvoudigt. Toch het betekent ook dat de server geen plek heeft waar hij “deze is niet meer geldig” kan noteren. Zolang de handtekening klopt en de vervaldatum niet is bereikt, is het token geldig, ongeacht wat er intussen met het account is gebeurd.
Het lijkt op een toegangsbewijs met een datum erop in plaats van een pas die wordt gescand. De portier controleert of het echt is en of de datum klopt; hij heeft geen manier om te weten dat dit specifieke bewijs vanochtend is ingetrokken.
Hoe wordt een JWT die niet is in te trekken misbruikt?
Kwetsbaar:
// Token met een ruime geldigheidsduur, geen enkele vorm van intrekking
function maakToken(gebruiker) {
return jwt.sign(
{ sub: gebruiker.id, rol: gebruiker.rol },
SLEUTEL,
{ expiresIn: '24h' },
);
}
app.post('/uitloggen', (req, res) => {
res.clearCookie('token'); // alleen de browser
res.send('Uitgelogd');
});
function controleer(req, res, next) {
req.gebruiker = jwt.verify(req.cookies.token, SLEUTEL);
next(); // handtekening klopt, dus toegang
}
Het uitloggen verwijdert het token uit de browser, maar het token zelf blijft vierentwintig uur geldig. Wie een kopie heeft (uit een logbestand, via cross-site scripting, of van een gedeelde computer) komt er de rest van de dag mee binnen. Dat geldt onverkort na een wachtwoordwijziging, en zelfs nadat het account is geblokkeerd of verwijderd: het token draagt zijn eigen rechten mee en wordt nergens tegen de werkelijkheid gecontroleerd.
Veilig:
// Kort access token met een unieke identificatie, plus een intrekbaar refresh token
function maakAccessToken(gebruiker) {
return jwt.sign(
{ sub: gebruiker.id, rol: gebruiker.rol, jti: randomUUID() },
SLEUTEL,
{ expiresIn: '10m' },
);
}
app.post('/uitloggen', async (req, res) => {
const payload = jwt.verify(req.cookies.token, SLEUTEL);
// Alleen tot de oorspronkelijke vervaldatum onthouden; daarna is het overbodig
const resterend = payload.exp - Math.floor(Date.now() / 1000);
await cache.set(`ingetrokken:${payload.jti}`, '1', { EX: resterend });
await refreshTokens.verwijder(req.cookies.refresh);
res.clearCookie('token');
res.clearCookie('refresh');
res.send('Uitgelogd');
});
async function controleer(req, res, next) {
const payload = jwt.verify(req.cookies.token, SLEUTEL);
if (await cache.get(`ingetrokken:${payload.jti}`)) {
return res.status(401).send('Token ingetrokken');
}
if (payload.iat < await gebruikers.tokensOngeldigVanaf(payload.sub)) {
return res.status(401).send('Opnieuw inloggen vereist');
}
req.gebruiker = payload;
next();
}
Er gebeuren hier drie dingen. Het access token is nog maar tien minuten geldig, waardoor het venster voor misbruik klein is. Elk token krijgt met jti een unieke identificatie, zodat het gericht kan worden ingetrokken. Bovendien er is een tijdstempel per gebruiker: door bij een wachtwoordwijziging tokensOngeldigVanaf op nu te zetten, sneuvelen in één keer álle tokens die daarvoor zijn uitgegeven, zonder dat u ze afzonderlijk hoeft te kennen.
localStorage. Die opslag is met JavaScript uitleesbaar, waardoor elke cross-site-scriptingkwetsbaarheid direct het token oplevert, en juist bij een token dat u niet kunt intrekken is dat een langdurig probleem. Gebruik een cookie met HttpOnly, Secure en SameSite.Wat is de impact van een JWT die niet is in te trekken?
De ernst loopt van middelzwaar tot hoog, en dat hangt vooral af van de geldigheidsduur. Bij tokens van enkele minuten is het venster klein; bij tokens die een dag of langer meegaan is het risico aanzienlijk.
Het probleem is dat uitloggen en het wijzigen van een wachtwoord de handelingen zijn waarmee een gebruiker of beheerder de controle heropneemt. Vermoedt iemand dat zijn account is gecompromitteerd, dan verwacht hij dat het wijzigen van zijn wachtwoord de indringer buitensluit. Bij een niet-intrekbaar token is dat niet zo: de aanvaller houdt zijn toegang tot het token vanzelf verloopt.
Nog scherper wordt het bij het beëindigen van een dienstverband of het intrekken van rechten. Een medewerker wiens account ’s ochtends wordt geblokkeerd, kan met een geldig token de rest van de dag blijven werken alsof er niets is gebeurd. Draagt het token bovendien de rol mee, dan blijven ook zijn oude rechten intact nadat die zijn afgenomen. Dat is precies het soort scenario waarop een audit toetst.
Hoe spoor je een JWT die niet is in te trekken op?
Een tester logt in, legt het token vast, logt uit en gebruikt het token opnieuw op een beschermde route. Wordt het geaccepteerd, dan is er geen intrekking. Dezelfde test wordt herhaald na een wachtwoordwijziging, na het intrekken van rechten en na het blokkeren van het account, die drie gaan vaker mis dan het uitloggen zelf.
Daarnaast wordt de inhoud van het token bekeken. Hoe lang is de geldigheidsduur? Zit er een jti in waarmee gericht kan worden ingetrokken? Draagt het token rollen of rechten mee die daarmee bevroren zijn tot de vervaldatum? Wordt er met refresh-tokens gewerkt, en worden die bij gebruik geroteerd? Ook wordt gecontroleerd waar het token wordt bewaard, omdat opslag in localStorage het risico van een niet-intrekbaar token aanzienlijk vergroot. AssistSec toetst deze punten in samenhang, omdat een korte geldigheidsduur zonder intrekking soms acceptabel is en een lange geldigheidsduur zonder intrekking dat vrijwel nooit is.
Hoe voorkom je een JWT die niet is in te trekken?
- Geef access tokens een korte geldigheidsduur, in de orde van vijf tot vijftien minuten.
- Werk met refresh-tokens die u aan serverzijde bewaart en dus wél kunt intrekken.
- Roteer refresh-tokens bij elk gebruik en trek de hele familie in zodra een oud token opnieuw wordt aangeboden.
- Geef elk token een unieke
jti, zodat het gericht kan worden ingetrokken. - Houd per gebruiker een tijdstempel bij waarvóór uitgegeven tokens ongeldig zijn, en zet die bij wachtwoord- en rechtenwijzigingen.
- Bewaar tokens in cookies met
HttpOnly,SecureenSameSite, niet inlocalStorage. - Zet geen rollen of rechten in het token die tijdens de looptijd kunnen wijzigen; zoek die bij voorkeur op.
- Houd de intrekkingslijst klein door ingetrokken identificaties automatisch te laten verlopen op hun oorspronkelijke vervaldatum.
Bronnen
Veelgestelde vragen
Verlies ik de voordelen van JWT met een intrekkingslijst?
Deels, maar minder dan gevreesd. U hoeft niet elk token in een database op te zoeken; het volstaat om een korte lijst van ingetrokken identificaties te raadplegen, bijvoorbeeld in een cache in het geheugen. Omdat die lijst alleen tokens bevat die nog niet zijn verlopen, blijft hij klein.
Hoe lang mag een access token geldig zijn?
Vijf tot vijftien minuten is gangbaar. Dat venster is kort genoeg om de schade van een gestolen token te beperken en lang genoeg om het aantal vernieuwingen beheersbaar te houden. De langere geldigheid verhuist naar het refresh-token, dat u wél kunt intrekken.
Wat is tokenrotatie bij refresh-tokens?
Bij elke vernieuwing geeft de server een nieuw refresh-token uit en maakt hij het oude ongeldig. Wordt een oud token later alsnog gebruikt, dan is dat een sterk signaal dat iemand een kopie heeft; de server trekt dan de hele tokenfamilie in. Zo wordt diefstal zowel beperkt als detecteerbaar.
Kan ik tokens niet gewoon in de browser verwijderen?
Dat is precies de valkuil. Het token uit de opslag van de browser halen laat het token zelf ongemoeid: het is nog steeds correct ondertekend en nog niet verlopen. Iedereen die een kopie heeft, blijft ermee binnenkomen. Intrekken moet aan serverzijde gebeuren.
Verwante artikelen
- KwetsbaarhedenCWE-287A07:2021Broken authenticationBroken authentication uitgelegd: hoe aanvallers via brute force, gelekte wachtwoorden en voorspelbare sessietokens accounts overnemen, en wat u eraan doet.
- KwetsbaarhedenCWE-347A07:2021JWT-kwetsbaarhedenJWT-kwetsbaarheden uitgelegd: alg:none, algoritmeverwarring, ontbrekende controles en hoe u JSON Web Tokens veilig verifieert.
- KwetsbaarhedenCWE-613A07:2021Sessie blijft geldig na uitloggenUitloggen dat alleen de cookie wist, laat het token aan serverzijde intact. Lees hoe een buitgemaakte sessie dan gewoon bruikbaar blijft.
- KwetsbaarhedenCWE-613A07:2021Sessies verlopen nietEen sessie zonder vervaltijd blijft eindeloos bruikbaar. Lees waarom dat gestolen tokens waardevol maakt en hoe u een verstandige timeout instelt.