Contact
Type
Analyse
Publié le
Durée
6 min de lecture
Auteur
Équipe rootzero

JWT : cinq erreurs qui ouvrent la porte

alg none, confusion d’algorithme, secret HMAC faible, injection via kid, jetons sans expiration : cinq erreurs JWT classiques et leurs correctifs.

  • Web
  • API
  • Authentification

Un JSON Web Token est un format : un en-tête, une charge utile et une signature, encodés en Base64url et séparés par des points. Le format ne protège rien par lui-même. Toute la sécurité dépend de la façon dont le serveur vérifie le jeton, et c’est à cet endroit que se concentrent les erreurs.

Les cinq erreurs décrites ici reviennent régulièrement en test d’intrusion d’API. Aucune n’est nouvelle, et la plupart figurent dans les bonnes pratiques publiées par l’IETF en 2020 (RFC 8725). Elles persistent parce que chacune part d’une hypothèse qui semble raisonnable quand on écrit le code.

Rappel sur la structure

Un JWT signé (RFC 7519, qui s’appuie sur JWS, RFC 7515) se découpe ainsi :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiIxMDQyIiwi... . 3q2-7w...
└──────────── en-tête ─────────────┘   └──── charge utile ───┘   └ signature ┘

L’en-tête décodé indique l’algorithme ({"alg":"HS256","typ":"JWT"}). La charge utile porte les claims : sub, exp, iss, aud, rôles, etc. Le point qui explique presque toutes les erreurs suivantes : l’en-tête est fourni par le client. Les valeurs alg, kid ou jku sont des données non fiables tant que la signature n’a pas été vérifiée, et elles ne doivent jamais décider de la manière de la vérifier.

Notre JWT Inspector décode un jeton dans le navigateur, sans l’envoyer à un serveur, et signale plusieurs des problèmes décrits ici.

1. Accepter alg: none

La spécification JWA (RFC 7518) définit un algorithme none pour les jetons non sécurisés, c’est-à-dire sans signature. Si la bibliothèque lit l’algorithme dans l’en-tête et l’applique tel quel, n’importe qui peut fabriquer un jeton accepté :

# En-tête {"alg":"none","typ":"JWT"}, charge utile {"sub":"1042","role":"admin",...}
# La troisième partie est vide : aucune signature.
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMDQyIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzg4MDAwMDAwfQ.

Les bibliothèques répandues refusent aujourd’hui ce cas par défaut. Le défaut réapparaît dans le code maison, dans de vieilles surcouches et dans les filtres qui oublient les variantes de casse (None, NONE). Il a été largement documenté en 2015 dans l’article Critical vulnerabilities in JSON Web Token libraries.

Le correctif consiste à fixer côté serveur la liste des algorithmes acceptés, sans jamais la déduire du jeton.

2. La confusion d’algorithme RS256 vers HS256

C’est une variante plus subtile du même défaut. Une API signe ses jetons en RS256 : la clé privée signe, la clé publique vérifie. Cette clé publique est souvent exposée dans un point de terminaison JWKS, ce qui est normal.

Supposons que le code de vérification ressemble à verify(token, key) et laisse l’en-tête choisir l’algorithme. Un attaquant peut alors :

  1. récupérer la clé publique RSA ;
  2. fabriquer un jeton dont l’en-tête annonce "alg": "HS256" ;
  3. le signer en HMAC en prenant la clé publique comme secret.

La bibliothèque lit HS256, traite la clé fournie (la clé publique au format PEM, donc du texte) comme un secret HMAC, et la signature se vérifie. L’attaquant a signé un jeton valide sans jamais disposer de la clé privée.

Le correctif combine une liste d’algorithmes explicite et des objets clés typés, de sorte qu’une clé RSA ne puisse pas servir de secret HMAC. Avec PyJWT :

import jwt  # PyJWT >= 2

claims = jwt.decode(
    token,
    key=public_key,                 # objet clé RSA chargé au démarrage
    algorithms=["RS256"],           # fixé par le serveur
    audience="api.exemple.fr",
    issuer="https://auth.exemple.fr/",
    options={"require": ["exp", "iat", "sub"]},
    leeway=30,                      # tolérance d'horloge, en secondes
)

En Node.js, la bibliothèque jose suit la même logique :

import { jwtVerify, createRemoteJWKSet } from 'jose';

const jwks = createRemoteJWKSet(new URL('https://auth.exemple.fr/.well-known/jwks.json'));
const { payload } = await jwtVerify(token, jwks, {
  algorithms: ['RS256'],
  issuer: 'https://auth.exemple.fr/',
  audience: 'api.exemple.fr',
});

3. Un secret HMAC devinable

En HS256, le jeton est exactement aussi solide que le secret partagé. Un JWT signé contient tout ce qu’il faut pour une attaque hors ligne : le message (en-tête et charge utile) et sa signature. Avec un seul jeton capturé, un attaquant peut tester des milliards de secrets candidats sur sa propre machine, sans jamais solliciter votre serveur et donc sans déclencher d’alerte. Hashcat prend ce format en charge nativement (mode 16500, voir ses exemples de hachages).

Les premiers candidats testés sont prévisibles : secret, changeme, le nom de l’application, la valeur d’exemple de la documentation du framework.

La RFC 7518 (section 3.2) impose une clé au moins aussi longue que la sortie de la fonction de hachage, soit 256 bits pour HS256. Générez-la avec une source aléatoire cryptographique :

# 32 octets aléatoires = 256 bits
openssl rand -base64 32
# ou en Python
python3 -c "import secrets; print(secrets.token_urlsafe(32))"

Rangez-la dans un gestionnaire de secrets, hors du dépôt Git, et prévoyez sa rotation (c’est l’un des usages légitimes de kid). Si plusieurs services vérifient les jetons, la signature asymétrique est préférable : seuls les émetteurs détiennent alors la clé privée.

4. Faire confiance à kid et à jku

Le paramètre kid (key ID) désigne la clé qui a servi à signer, ce qui facilite la rotation. Il devient dangereux dès que le serveur s’en sert tel quel pour aller chercher la clé.

  • Utilisé comme chemin de fichier, kid: "../../../../dev/null" fait charger un fichier vide, et un jeton signé avec une clé vide peut alors être accepté.
  • Concaténé dans une requête SQL du type SELECT key FROM keys WHERE id = '…', il ouvre une injection SQL classique, qui peut faire renvoyer une clé choisie par l’attaquant.
  • Plus rarement, on le retrouve dans une commande système, au sein d’intégrations maison.

Les paramètres jku et x5u posent un problème voisin, car ils désignent une URL où trouver la clé. Si le serveur suit cette URL, l’attaquant y héberge sa propre clé et signe ce qu’il veut. Le même mécanisme permet aussi de faire émettre des requêtes au serveur (SSRF).

Le correctif traite kid comme un identifiant opaque, recherché dans une table fixe de clés connues ; une valeur inconnue entraîne un refus. jku et x5u sont ignorés, ou comparés à une liste blanche stricte d’URL.

TRUSTED_KEYS = {"2026-09": key_sept, "2026-10": key_oct}  # chargé au démarrage

def key_for(token: str):
    kid = jwt.get_unverified_header(token).get("kid")
    if kid not in TRUSTED_KEYS:
        raise jwt.InvalidTokenError("kid inconnu")
    return TRUSTED_KEYS[kid]

5. Des jetons sans expiration ni révocation

Un JWT est autoporteur : le serveur le valide sans consulter d’état. C’est son intérêt et sa limite. Un jeton sans exp, ou valable plusieurs mois, survit à la déconnexion, au changement de mot de passe et au départ de l’employé. S’il fuit (journal applicatif, en-tête collé dans un ticket, extension de navigateur malveillante), il reste exploitable pendant toute cette durée.

Plusieurs mesures se combinent en pratique. Le jeton d’accès vit peu de temps, de quelques minutes à une heure selon la sensibilité. Un jeton de rafraîchissement, conservé côté serveur et à usage unique, est remplacé à chaque utilisation ; sa réutilisation signale un vol et doit invalider toute la session. Une liste de révocation sur jti couvre les urgences, et sa taille reste bornée puisque les jetons expirent vite. Enfin, le serveur vérifie iss, aud et nbf en plus de la signature, car un jeton correctement signé pour un autre service n’a rien à faire chez vous.

Liste de contrôle avant mise en production

Contrôle Attendu
Algorithmes Liste fixée côté serveur, none refusé
Clés Objets typés ; une clé publique ne sert jamais de secret HMAC
Secret HMAC Au moins 256 bits aléatoires, hors du code, rotation prévue
kid Recherche dans une table fixe, valeur inconnue refusée
jku, x5u Ignorés, ou liste blanche stricte
Claims exp obligatoire ; iss, aud et nbf vérifiés
Durée de vie Courte, rafraîchissement avec rotation
Révocation Possible et testée

L’OWASP tient à jour une fiche dédiée aux JWT qui détaille chacun de ces points. Pour examiner un jeton réel sans le confier à un service tiers, le JWT Inspector fonctionne entièrement dans votre navigateur.

#Outil gratuit

Inspectez vos jetons, localement.

Le JWT Inspector décode en-tête et claims, vérifie une signature HMAC et signale alg none ou une expiration absente. Rien ne quitte votre navigateur.