Hai appena implementato l'autenticazione JWT nella tua API. I test passano, il frontend riceve il token, tutto sembra funzionare. Poi, per sbaglio, provi a modificare il payload del token e lo rispedisci al server. E il server lo accetta. Se non verifichi la firma, qualunque attaccante può impersonare qualsiasi utente.
Noi, di Meteora Web, vediamo questo errore nei progetti che ereditiamo: JWT spacciati per sicuri, ma senza alcun controllo reale sulla provenienza. Veniamo dalla contabilità e dal codice: sappiamo che un baco nella verifica della firma è come un buco nei libri contabili. Prima o poi viene fuori, e costa caro.
In questa guida ti mostriamo come firmare e verificare i JWT in modo robusto, e soprattutto quali vulnerabilità evitare. Tutto con esempi reali in PHP e Node.js, pronti da copiare e testare.
Come funziona la firma dei JWT e perché è critica per la sicurezza
Un JWT è composto da tre parti separate da punti: header, payload, signature. La signature è ciò che garantisce l'integrità del token. Senza di essa, chiunque può modificare il payload (es. cambiare il ruolo da "user" a "admin") e il server non se ne accorge.
La firma si genera applicando un algoritmo di hashing (HMAC o RSA/ECDSA) a header + payload + una chiave segreta (HMAC) o una coppia di chiavi (asimmetrico). Il server, quando riceve il token, ricalcola la firma con la stessa chiave e confronta. Se non corrisponde, rifiuta il token.
Algoritmi supportati: una scelta che fa la differenza
I JWT supportano molti algoritmi: HS256, HS384, HS512 (HMAC con SHA-2), RS256, RS384, RS512 (RSA con SHA-2), ES256, ES384, ES512 (ECDSA).
Sponsored Protocol
La scelta tra simmetrico (HMAC) e asimmetrico (RSA/ECDSA) dipende dal contesto:
- HMAC: stessa chiave per firmare e verificare. Ideale quando client e server sono nello stesso dominio (unica applicazione). La chiave deve rimanere segreta sul server.
- RSA/ECDSA: chiave privata per firmare, chiave pubblica per verificare. Perfetto se il token viene emesso da un server di autenticazione e verificato da servizi diversi (microservizi, API pubbliche).
Errore comune: usare HMAC con la stessa chiave per emettere e verificare token, ma esporre la chiave in un client nativo o JavaScript. Chiunque estrae la chiave e genera token validi.
Quali sono le vulnerabilità più comuni dei JWT
Le vulnerabilità sui JWT non sono astratte: sono sfruttate ogni giorno in attacchi reali. Ecco le più frequenti.
Algorithm confusion attack
L'attaccante modifica l'header del JWT impostando "alg": "none". Se il server non verifica esplicitamente che l'algoritmo sia tra quelli consentiti, accetta token senza firma. Variante: cambia da RS256 a HS256. Se il server si aspetta chiave pubblica (RSA) ma riceve HMAC, usa la chiave pubblica (spesso nota) per verificare la firma HMAC. L'attaccante firma il token con la chiave pubblica e il server lo approva.
Come proteggersi: non accettare mai l'algoritmo dall'input. Imposta una whitelist fissa nel codice.
Key confusion attack
Simile al precedente, ma focalizzato sullo scambio di chiavi. Se il server usa la stessa variabile per chiave HMAC e chiave pubblica, un attaccante può forzare l'uso di una chiave pubblica nota per firmare token HMAC.
Sponsored Protocol
Missing signature verification
Sembra banale, ma capita. Librerie con API che restituiscono il payload direttamente senza verificare la firma. Ad esempio, jwt.decode() in Python senza specificare verify=True. In Node.js, la funzione jwt.verify() va usata, non jwt.decode().
Token replay e scadenza non controllata
Un token valido può essere riutilizzato indefinitamente se non si imposta exp e se non si implementa un blacklist per token revocati. In più, se nbf (not before) non viene controllato, token futuri vengono accettati.
Come verificare la firma di un JWT in modo robusto
Vediamo implementazioni concrete. Partiamo dallo scenario più comune: server che emette e verifica token (HMAC). Poi passiamo al caso asimmetrico.
PHP con firebase/php-jwt
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// FIRMA (emissione)
$key = 'una-chiave-segreta-molto-lunga-e-casuale!';
$payload = [
'sub' => 123,
'name' => 'Mario Rossi',
'iat' => time(),
'exp' => time() + 3600
];
$jwt = JWT::encode($payload, $key, 'HS256');
// VERIFICA
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
print_r($decoded);
// Se la firma non è valida o il token è scaduto, JWT::decode lancia un'eccezione.
Attenzione: JWT::decode accetta un array di chiavi oppure un singolo oggetto Key. Specifica sempre l'algoritmo nel secondo parametro. Non usare mai JWT::decode($jwt, $key) senza Key, perché accetta qualsiasi algoritmo fino alla versione 5.x (vulnerabilità nota).
Sponsored Protocol
Node.js con jsonwebtoken
const jwt = require('jsonwebtoken');
// FIRMA
const secret = 'una-chiave-segreta-molto-lunga';
const token = jwt.sign(
{ sub: 123, name: 'Mario Rossi' },
secret,
{ algorithm: 'HS256', expiresIn: '1h' }
);
// VERIFICA
jwt.verify(token, secret, { algorithms: ['HS256'] }, (err, decoded) => {
if (err) {
console.error('Token non valido:', err.message);
return;
}
console.log(decoded);
});
Regola chiave: nell'opzione algorithms specifica solo gli algoritmi che accetti. Se non lo fai, la libreria accetta di default qualsiasi algoritmo, esponendoti all'alg confusion attack.
Caso asimmetrico (RS256)
Se usi coppia di chiavi, la firma avviene con la chiave privata e la verifica con quella pubblica. In PHP:
$privateKey = file_get_contents('/path/to/private.pem');
$publicKey = file_get_contents('/path/to/public.pem');
// FIRMA
$jwt = JWT::encode($payload, $privateKey, 'RS256');
// VERIFICA
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
In Node.js:
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Mai mischiare chiavi simmetriche e asimmetriche nello stesso progetto. Usa un unico approccio per dominio.
Sponsored Protocol
Come proteggere i JWT da attacchi di tipo alg confusion e key confusion
La difesa è una combinazione di codice solido e buone pratiche architetturali.
Whitelist fissa degli algoritmi
Nel codice, non leggere mai l'algoritmo dall'header del token per decidere come verificare. Imposta una costante o variabile d'ambiente con la lista degli algoritmi accettati.
$allowed_algs = ['HS256']; // oppure ['RS256']
E passa sempre questa lista alla verifica.
Validazione del token a più livelli
- Struttura: verifica che il token abbia esattamente tre parti separate da punti.
- Header: controlla che
typsiaJWTe che l'algsia nella whitelist. - Payload: verifica i claim standard (
exp,nbf,iat,iss,aud). - Firma: ricalcola la firma con la chiave e l'algoritmo atteso.
Alcune librerie fanno già tutto, ma se usi una libreria minimale, implementa tu questi passi.
Gestione sicura delle chiavi
- Mai hardcodare chiavi nel codice. Usa variabili d'ambiente o vault.
- Ruota periodicamente le chiavi HMAC o le coppie RSA/ECDSA.
- Per le chiavi asimmetriche, non esporre la chiave privata. La chiave pubblica può essere distribuita, ma verificane la provenienza (certificato, JWKS endpoint).
Impedire il replay token
Oltre a exp (breve, 15-60 minuti), implementa un meccanismo di revoca per token blacklistati (es. logout, cambio password). Un approccio semplice: tieni un ID univoco nel token (jti) e memorizza in un database (Redis con TTL) i token revocati. Ad ogni richiesta, controlla se il jti è nella blacklist.
Sponsored Protocol
$blacklisted = $redis->get('blacklist:' . $decoded->jti);
if ($blacklisted) {
throw new Exception('Token revocato');
}
Cosa fare adesso
Abbiamo visto che un JWT è sicuro solo se la firma viene verificata con l'algoritmo giusto e la chiave corretta. Basta un dettaglio — un decode() al posto di verify(), un algoritmo non forzato — per aprire la porta a un attacco.
Azioni immediate da compiere oggi:
- Controlla il tuo codice di verifica: stai usando
jwt.verify()oJWT::decodeconKey? Se usi una libreria senza specificare l'algoritmo, correggi subito. - Aggiungi una whitelist di algoritmi nel tuo middleware di autenticazione. Non lasciare che sia l'header del token a decidere.
- Implementa la blacklist dei token revocati con Redis o database. Soprattutto se hai logout attivo o cambio password frequente.
- Rivedi la durata dei token:
exptroppo lungo aumenta il rischio di furto. Punta a 15-60 minuti per access token, e usa refresh token con rotazione. - Esegui un audit di sicurezza su tutto il flusso JWT: dal server di emissione ai client che ricevono il token. Noi, di Meteora Web, lo facciamo regolarmente sui progetti che ci vengono affidati. Se vuoi un check approfondito, contattaci.
Per approfondire la sicurezza a 360°, leggi anche la nostra guida pillar su crittografia e sicurezza dati e la guida sul vulnerability scanning per individuare falle nella tua infrastruttura.