Aller au contenu

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.

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.

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"}

L’objectif : partir d’un token légitime, faire passer le claim role de user à admin, forcer l’algorithme à none et supprimer la signature.

Fenêtre de terminal
# En-tête forcé à none, puis encodage Base64URL sans padding
echo -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.

jwt_tool industrialise l’attaque et teste plusieurs variantes de casse (none, None, NONE, nOnE), certaines bibliothèques ne filtrant que la forme minuscule.

Fenêtre de terminal
python3 jwt_tool.py <TOKEN> -X a

Si la réponse renvoie un contenu authentifié ou des droits élevés, le serveur ne vérifie pas la signature.

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.

Fenêtre de terminal
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.

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 passe
jwt.decode(token, key)
# Correct : algorithms verrouille la liste acceptée
jwt.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érifiable
Jwts.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.

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.

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.