Sicurezza JWT: attacchi, difese e una checklist per il 2026
I JSON Web Token reggono buona parte dell’autenticazione moderna, eppure le best practice di sicurezza che li tengono al riparo vengono ignorate molto più spesso di quanto dovrebbero. Il JWT è ormai il formato di credenziale di riferimento per OAuth 2.0, OpenID Connect e le chiamate service-to-service nei microservizi. È anche all’origine di un flusso costante di CVE ogni anno, e quasi tutte risalgono agli stessi errori evitabili: accettare token non firmati, fidarsi dell’algoritmo scelto da un attaccante, usare un segreto di firma debole o saltare la validazione dei claim.
Un JWT è sicuro quando reggono quattro condizioni contemporaneamente. La firma è integra, l’attaccante non può scambiare l’algoritmo, i claim vengono davvero controllati e il token è custodito dove non può essere rubato con facilità. Basta che una sola venga meno e hai un bypass dell’autenticazione, non un’API blindata. Questa guida ripercorre i tre attacchi che contano di più, poi le difese: scegliere e fissare un algoritmo, gestire le chiavi, validare i claim e custodire i token. Si chiude con una checklist da incollare in una code review.
Come una firma JWT ti protegge davvero (e cosa invece non copre)
Prima che un qualsiasi attacco abbia senso, serve mettere a fuoco un fatto: un JWT è codificato, non cifrato. Un token firmato ha tre segmenti Base64URL uniti da punti, cioè header.payload.signature. Header e payload sono semplice Base64URL di JSON. Chiunque possieda il token può leggere ogni claim al suo interno. Incolla un token qualsiasi nel nostro decodificatore JWT e vedrai header e payload esposti in JSON leggibile, senza bisogno di alcuna chiave. Il payload è pubblico per definizione.
Da dove arriva allora la sicurezza? Dalla firma, e solo dalla firma. È un valore crittografico calcolato su header e payload con un segreto (HMAC) o una chiave privata (RSA, ECDSA). Un attaccante può leggere un token liberamente, ma non può produrre un token diverso che superi la verifica senza la chiave di firma. Tutto il modello di fiducia si regge su questa singola proprietà.
Ne derivano due conseguenze. Primo: non mettere mai segreti nel payload (password, chiavi API, dati personali completi), perché è leggibile da chiunque intercetti il token. Secondo: l’intera tua postura di sicurezza poggia su un solo passaggio, verificare correttamente la firma. Ed è esattamente il passaggio che gli attaccanti prendono di mira. Se vuoi una spiegazione più approfondita su come leggere un token segmento per segmento, vedi come decodificare un JWT.
I 3 attacchi JWT critici (e come fermare ciascuno)
La maggior parte delle vulnerabilità JWT è una variazione sullo stesso tema: il server si fida di qualcosa che l’attaccante controlla. Ecco i tre che mandano in pezzi l’autenticazione, con il meccanismo dietro ognuno e la relativa correzione.
1. L’attacco alg:none: bypass tramite token non firmato
La specifica JWS prevede un valore alg pari a none, che significa “non firmato”. Un token alg:none ha il segmento della firma vuoto e termina comunque con un punto finale, come header.payload.. L’attacco è semplice: prendi un token valido, cambi l’alg nell’header in none, sostituisci i claim a piacere (per esempio "role": "admin") ed elimini la firma. Le prime librerie JWT lo accettavano in modo predefinito, così il token contraffatto superava indisturbato la verifica. Nessuna chiave, nessuna firma, impersonificazione completa.
Puoi vedere com’è fatto un token del genere caricando l’esempio “alg:none” nel nostro decodificatore JWT: mostra un avviso rosso esplicito che segnala che il token è non firmato e non deve mai essere accettato per l’autenticazione. Riprodurne uno da solo è un esercizio di un minuto per capire la minaccia.
La difesa è una allowlist esplicita degli algoritmi su ogni chiamata di verifica. Non lasciare mai decidere al valore predefinito della libreria cosa è accettabile, perché i vecchi default erano permissivi e il costo di essere espliciti è una sola opzione in più.
// WRONG — the library may accept alg:none or any algorithm
jwt.verify(token, key);
// RIGHT — pin the exact algorithm you expect
jwt.verify(token, key, { algorithms: ['RS256'] });
none non dovrebbe mai comparire in quell’array. Se la tua libreria non sa fissare gli algoritmi, sostituiscila.
2. Confusione di algoritmo: RS256 declassato a HS256
È la vulnerabilità JWT più pericolosa nella pratica, nota dal 2015 e tuttora ritrovata negli audit di oggi. Sfrutta i server che decidono come verificare in base al campo alg nell’header, l’unica parte del token che un attaccante può riscrivere.
Ecco il meccanismo. Il tuo server emette token RS256: firma con una chiave privata RSA e verifica con la chiave pubblica corrispondente. Quella chiave pubblica è, per definizione, pubblica: può trovarsi nel tuo endpoint JWKS o nel tuo repository. L’attaccante la prende, cambia l’header del token da RS256 a HS256 e firma un payload contraffatto usando HMAC-SHA256 con la stringa della chiave pubblica come segreto HMAC. Ora il lato verifica: se il tuo codice legge alg dall’header e sceglie HMAC di conseguenza, calcola HMAC-SHA256 sul token usando quella stessa chiave pubblica come segreto. Le firme coincidono. Il token contraffatto viene accettato.
La causa profonda è lo scontro di due fatti: il verificatore si è fidato dell’header alg controllato dall’attaccante, e la chiave pubblica RSA era a disposizione dell’attaccante per usarla come chiave HMAC. Nessuno dei due fatti è un bug in sé. Una chiave pubblica deve essere pubblica, e un header alg deve descrivere il token. La vulnerabilità nasce nel momento in cui la tua logica di verifica lascia che quell’header scelga quale tipo di chiave e quale algoritmo usare: allora un valore che l’attaccante scrive guida il percorso crittografico che il server esegue.
// WRONG — verification method follows the header's alg field
jwt.verify(token, publicKeyOrSecret);
// RIGHT — hard-code the expected algorithm; never let the header choose
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Fissa esplicitamente l’algoritmo asimmetrico (solo RS256 o ES256), tieni la verifica HMAC su un percorso di codice del tutto separato dalla verifica RSA e usa una libreria mantenuta che distingua i tipi di chiave. Il nostro decodificatore JWT segnala qualsiasi token della famiglia HS con un avviso di confusione sulla chiave pubblica, proprio perché questo attacco è così diffuso: se un token che ti aspettavi asimmetrico si presenta come HS256, quell’avviso è il tuo campanello d’allarme.
3. Segreto HMAC debole: attacchi a forza bruta e a dizionario
Quando usi HMAC (HS256/384/512), l’intera sicurezza del token poggia sull’entropia di un solo segreto. Se quel segreto è corto, una parola da dizionario o un valore come secret o password123, un attaccante che cattura un singolo token valido può forzarlo offline. Strumenti come hashcat passano in rassegna miliardi di candidati al secondo contro la firma del token. Una volta caduto il segreto, l’attaccante può coniare qualsiasi token desideri: credenziali admin valide per sempre.
Ciò che rende questo attacco silenziosamente pericoloso è che è interamente offline. L’attaccante non martella il tuo endpoint di login, quindi non c’è alcun rate limit da far scattare e nulla nei tuoi log da notare. Cattura un token, forza il segreto sul proprio hardware e si rifà vivo solo quando è in grado di firmare token che superano ogni controllo che hai messo. La correzione non è negoziabile: usa almeno 32 byte casuali (256 bit) da una sorgente crittograficamente sicura e custodiscilo in un secrets manager, mai nel codice o in un repository.
// WRONG — guessable, low entropy, crackable in seconds
const secret = "password123";
// RIGHT — 256 bits from a CSPRNG, then load from KMS at runtime
const secret = require('crypto').randomBytes(32).toString('base64');
Ti serve un valore robusto in fretta? Il nostro generatore di password casuali produce stringhe ad alta entropia adatte a una chiave HMAC. Vuoi toccare con mano la differenza? Firma un token di prova con un segreto robusto nel nostro codificatore JWT, che gira interamente nel browser così il segreto non lascia mai la tua macchina. E quando la verifica attraversa un confine di fiducia (più servizi, verificatori di terze parti), smetti del tutto di usare HS256 e passa a un algoritmo asimmetrico.
Scegliere e fissare l’algoritmo giusto
La scelta dell’algoritmo è il terreno su cui l’attacco di confusione si vince o si perde, quindi scegli con criterio. I tre che userai nella pratica:
| Algoritmo | Tipo | Chiave di firma / verifica | Quando usarlo |
|---|---|---|---|
| HS256 | Simmetrico (HMAC) | Un solo segreto condiviso | Confine di fiducia unico, stessa parte firma e verifica |
| RS256 | Asimmetrico (RSA) | La chiave privata firma / la chiave pubblica verifica | Cross-service, verifica di terze parti, rotazione JWKS |
| ES256 | Asimmetrico (ECDSA) | La chiave privata firma / la chiave pubblica verifica | Come RS256, chiavi più piccole e veloci, da preferire per i nuovi sistemi |
La regola è breve. Se la stessa parte firma e verifica dentro un unico confine di fiducia, HS256 va bene ed è veloce. Se qualcuno diverso dal firmatario deve verificare (un altro servizio, un partner, un client pubblico), usa un algoritmo asimmetrico, e preferisci ES256: le sue chiavi e firme sono molto più piccole di RSA a parità di robustezza. Puoi firmare token di esempio HS256, RS256 ed ES256 fianco a fianco nel codificatore JWT per confrontarne struttura e lunghezza della firma.
Qualunque cosa tu scelga, la difesa che conta davvero è sempre la stessa: fissa un insieme esplicito di algoritmi sulla chiamata di verifica e non fidarti mai del campo alg nell’header. L’allowlist è la fondazione su cui poggia tutto il resto.
Gestione e rotazione delle chiavi
Gli algoritmi sono sicuri solo quanto le chiavi che li sostengono, e la gestione delle chiavi è il punto in cui la maggior parte delle guide ammutolisce. Per HS256 il segreto è di almeno 32 byte casuali e vive in un secrets manager come AWS Secrets Manager, HashiCorp Vault o Azure Key Vault. Per gli algoritmi asimmetrici la chiave privata sta in un HSM o KMS e non sfiora mai il codice applicativo; la chiave pubblica viene pubblicata, di norma tramite un endpoint JWKS da cui i verificatori la prelevano.
La rotazione deve essere ordinaria, non un’emergenza. Marca ogni chiave con un kid (key ID) nell’header del JWT, così i verificatori sanno quale chiave ha firmato un dato token. Tieni un piccolo insieme di chiavi valide sul lato verifica, la chiave corrente più la precedente recente, così i token firmati appena prima di una rotazione si verificano comunque per tutta la loro durata. È questa sovrapposizione a rendere la rotazione trasparente anziché un disservizio.
Una breve checklist per le chiavi:
- Ruota le chiavi di firma almeno ogni 90 giorni, e immediatamente al minimo sospetto di compromissione.
- Pubblica le chiavi pubbliche tramite JWKS; versionale con
kid. - Tieni chiavi private e segreti HMAC in un KMS o HSM: mai in git, mai nel codice client, mai hard-coded.
- In caso di fuga, ruota la chiave e revoca subito i refresh token in circolazione.
La validazione dei claim che non puoi saltare
Il controllo della firma dimostra che un token è autentico. Non dimostra che il token è per te, adesso. Questo è il compito della validazione dei claim, ed è la difesa più economica che tu possa aggiungere. Cinque claim vanno controllati a ogni richiesta:
exp(expiration): rifiuta i token la cui scadenza è già passata.nbf(not before): rifiuta i token usati prima che si apra la loro finestra di validità.iat(issued at): facoltativamente rifiuta i token implausibilmente vecchi.iss(issuer): conferma che il token provenga dall’emittente di cui ti fidi.aud(audience): conferma che il token sia stato coniato per il tuo servizio. Un controlloaudmancante è la falla silenziosa più comune, perché consente di replayare contro un’altra API un token emesso per la prima.
La maggior parte delle librerie li valida al posto tuo quando passi i valori attesi:
jwt.verify(token, key, {
algorithms: ['ES256'],
issuer: 'https://auth.example.com',
audience: 'api.example.com',
clockTolerance: 5, // seconds, for distributed clock skew
});
Concedi una piccola tolleranza di clock, cinque secondi è il valore tipico, così uno scostamento minimo tra i server non fa rifiutare token altrimenti validi. Resisti alla tentazione di alzarla: una tolleranza generosa allarga la finestra in cui un token scaduto continua a funzionare, che è esattamente ciò che exp esiste per chiudere. Per controllare a occhio i valori exp e iat di un token, buttalo nel decodificatore JWT e converti i timestamp con il nostro convertitore di timestamp Unix.
Durata del token e dove custodire i JWT
I controlli lato server sono solo metà della storia. Il punto in cui il client conserva il token decide quanto è facile rubarlo, e la modalità di conservazione è dove XSS e session hijacking si incontrano. Lo schema che regge: un access token a vita breve (15-60 minuti) abbinato a un refresh token separato, dalla vita più lunga e revocabile.
La decisione sulla conservazione si riduce a un unico compromesso:
| Luogo di conservazione | Esposizione a XSS | Rischio CSRF | Raccomandazione |
|---|---|---|---|
| localStorage | Alta: qualsiasi JavaScript nella pagina può leggerlo | Nessuno | Da evitare per i token di sessione |
| Cookie HttpOnly + Secure + SameSite=Strict | Bassa: invisibile a JavaScript | Richiede protezione CSRF | Consigliato per le sessioni |
Un token in localStorage è leggibile da qualsiasi script in esecuzione nella pagina, quindi un solo bug XSS fa trapelare l’intera sessione, e l’attaccante può replayarla dalla propria macchina per tutta la sua durata. Un cookie HttpOnly non può essere letto da JavaScript in alcun modo, il che riduce il danno di un XSS a ciò che un attaccante riesce a fare dentro una pagina viva: grave, ma non una credenziale rubata che può portarsi via. Il costo dell’approccio a cookie è che ora ti serve la protezione CSRF, dato che i cookie viaggiano automaticamente a ogni richiesta; SameSite=Strict più un token CSRF risolve la cosa. Tieni l’access token corto così una fuga ha un raggio d’azione ridotto, e metti il refresh token in un cookie HttpOnly, Secure, SameSite. Al logout o in caso di sospetta compromissione, revoca il refresh token lato server e ruota la chiave di firma. Per il contesto più ampio su XSS, CSRF e cookie sicuri, vedi la nostra guida sulle best practice di sicurezza web.
Checklist di sicurezza JWT
Scorri questa lista prima di mandare in produzione qualsiasi autenticazione basata su JWT:
- La verifica fissa una allowlist esplicita di algoritmi e rifiuta
alg:none. - La verifica asimmetrica hard-coda l’algoritmo atteso e non legge mai
algdall’header (blocca la confusione). - I segreti HS256 sono di almeno 32 byte casuali, caricati da un KMS.
- Le chiavi private vivono in un HSM/KMS; le chiavi pubbliche sono pubblicate via JWKS e versionate con
kid. - Le chiavi di firma ruotano almeno ogni 90 giorni.
- Ogni richiesta valida
exp,nbf,iat,isseaud, con una tolleranza di clock di 5 secondi o meno. - Gli access token durano 15-60 minuti; i refresh token vivono in un cookie
HttpOnly. - Nessun segreto nel payload: è codificato, non cifrato.
FAQ
Il JWT è sicuro per impostazione predefinita?
No. La sicurezza del JWT dipende dalla configurazione. Devi fissare l’algoritmo, rifiutare alg:none, usare un segreto o una chiave ad alta entropia e validare i claim. Le configurazioni di libreria predefinite o permissive consentono spesso bypass dell’autenticazione.
Qual è la vulnerabilità JWT più pericolosa?
La confusione di algoritmo, in cui RS256 viene declassato a HS256 e la chiave pubblica è usata come segreto HMAC. È nota dal 2015 eppure compare ancora negli audit, perché sfrutta i server che scelgono il metodo di verifica dal campo alg dell’header.
Dovrei usare HS256 o RS256?
Usa HS256 quando la stessa parte firma e verifica dentro un unico confine di fiducia. Usa RS256 o ES256 quando un altro servizio o una terza parte deve verificare, o quando ti serve la rotazione JWKS. Per i nuovi sistemi preferisci ES256: chiavi più piccole e veloci a parità di robustezza.
Dove dovrei custodire un JWT?
Per i token di sessione preferisci un cookie HttpOnly, Secure, SameSite, dato che JavaScript non può leggerlo e un singolo bug XSS non può rubarlo. Evita localStorage per i token di sessione: qualsiasi XSS fa trapelare l’intera sessione, pronta per il replay.
Ogni quanto dovrei ruotare le chiavi di firma JWT?
Ruota almeno ogni 90 giorni come prassi ordinaria, e immediatamente al minimo sospetto di compromissione. Versiona le chiavi con kid e tieni sul verificatore sia la chiave attiva sia la precedente recente, così i token firmati appena prima della rotazione restano validi.
Un JWT può essere manomesso?
Non senza la chiave di firma: nessun attaccante può forgiare un token che superi la verifica. Ma se il tuo server accetta alg:none, è vulnerabile alla confusione di algoritmo o usa un segreto debole, la firma può essere aggirata. Sono fallimenti di configurazione, non difetti del JWT in sé.
Quali claim devo validare?
Valida exp (expiration), nbf (not before), iat (issued at), iss (issuer) e aud (audience). Un controllo aud mancante è la vulnerabilità silenziosa più comune: consente di replayare contro un altro servizio un token destinato al primo.
Conclusione
La sicurezza JWT non è complicata, ma ogni strato deve reggere. La firma è la tua unica garanzia, quindi verificala correttamente. Fissa un solo algoritmo esplicito e non fidarti mai dell’alg nell’header. Usa chiavi robuste e ruotate, custodite in un KMS. Valida exp, nbf, iat, iss e aud a ogni richiesta. Custodisci i token dove XSS non può arrivare.
Per metterlo in pratica, incolla un token qualsiasi nel nostro decodificatore JWT per ispezionarne algoritmo e claim e cogliere i rischi di alg:none o di confusione HS, e usa il codificatore JWT per sperimentare con la firma interamente nel tuo browser: le tue chiavi non lasciano mai il tuo dispositivo.