Falsificación de JWT en python-jose con una clave HMAC vacía (CVE-2026-120459)
python-jose hasta 3.5.0 acepta un secreto HMAC vacío, así que un JWT falsificado se valida cuando la clave de la aplicación está vacía; sin versión corregida.
Resumen
- python-jose acepta una clave HMAC de longitud cero. Cuando la clave de verificación HS256, HS384 o HS512 de una aplicación se resuelve como vacía, un atacante puede calcular la firma con esa clave vacía y hacer que se acepten reclamaciones falsificadas.
- Todas las versiones hasta 3.5.0 están afectadas. La última versión sigue siendo 3.5.0, publicada el 2025-05-28, y no existe una versión corregida al 2026-11-09.
- El registro CVE la califica CVSS v4.0 9.1 Crítica y CVSS v3.1 7.4 Alta. La prueba de concepto del reportero es pública en el ticket #398; no se conoce explotación y el CVE no está en CISA KEV al 2026-11-09.
- Rechace las claves vacías en su propio código o pase a una biblioteca que lo haga. PyJWT 2.15.1 rechaza una clave HMAC vacía con
InvalidKeyError.
¿Estoy afectado?
Todas estas condiciones deben cumplirse:
- Verifica tokens JSON Web con
python-joseversión 3.5.0 o anterior, usando HS256, HS384 o HS512. - La clave de verificación puede resolverse como un valor vacío: una cadena vacía, bytes vacíos, o un JWK
octcuyo miembrokestá vacío. - Su aplicación confía en las reclamaciones de un token que un atacante puede proporcionar.
Un secreto fijo y no vacío no es explotable. La falla importa cuando la clave se ensambla en tiempo de ejecución. Los ejemplos son una variable de entorno con un valor predeterminado vacío, un punto de conexión JWKS que devuelve un k vacío, o una columna de base de datos que nunca se rellenó.
El ticket del reportero también señala que jwt.decode() pone algorithms en None por defecto, lo que omite la lista blanca de algoritmos. Es una debilidad distinta, no la falla de clave vacía, pero una aplicación puede combinar ambas y dejar que el atacante elija el algoritmo.
| Biblioteca | Versiones afectadas | Versión corregida |
|---|---|---|
| python-jose | 0 a 3.5.0 | Ninguna al 2026-11-09 |
Compruebe qué está instalado:
pip show python-jose
pip freeze | grep -i jose
grep -rn "python-jose" requirements*.txt pyproject.toml Pipfile.lock poetry.lock 2>/dev/null
Después busque cada llamada de verificación y el origen de su clave:
grep -rn "jwt.decode\|jws.verify" --include="*.py" .
¿Cómo lo corrijo?
No existe una versión corregida de python-jose. La corrección práctica es dejar de pasar material de clave vacío a la biblioteca, y migrar a una biblioteca mantenida que lo rechace.
- Añada una comprobación en cada sitio de verificación para rechazar una cadena vacía, bytes vacíos o un JWK
octcon unkvacío antes de que se ejecutejwt.decode():
# Reject empty HMAC key material before python-jose sees it.
from jose.exceptions import JWKError
def require_nonempty_hmac_key(key):
if isinstance(key, (str, bytes)) and len(key) == 0:
raise JWKError("HMAC key must not be empty")
if isinstance(key, dict) and key.get("kty") == "oct":
if not key.get("k"):
raise JWKError("oct JWK k must not be empty")
return key
- Pase una lista blanca de algoritmos explícita en cada llamada, aunque no sea la falla de clave vacía en sí:
claims = jwt.decode(token, require_nonempty_hmac_key(key), algorithms=["HS256"])
- Migre a PyJWT, que rechaza la clave vacía al prepararla. La comprobación está en
HMACAlgorithm.prepare_keyy lanzaInvalidKeyError("HMAC key must not be empty.").
import jwt as pyjwt
claims = pyjwt.decode(token, key, algorithms=["HS256"])
- Si la clave puede venir de un documento JWKS, valide el documento al obtenerlo y rechace cualquier clave
octcon unkvacío. No espere a que falle la llamada de verificación.
Validar el documento JWKS vale la pena incluso después de la migración. Un punto de conexión JWKS que devuelve {"kty":"oct","k":""} es un error de configuración por sí solo, y cualquier verificador que lo acepte como material de clave falla abierto. Rechace el documento y alerte cuando una clave simétrica tenga un k vacío.
Si no puede cambiar el código de inmediato, cambie la clave de verificación afectada por un valor aleatorio fuerte. Eso cierra la exposición solo si cada despliegue recibe el nuevo valor; una clave vacía sigue siendo explotable donde la configuración aún se resuelve como vacía.
¿Cuál es la causa raíz?
python-jose valida el tipo de una clave HMAC pero nunca su longitud, y la ruta JWK convierte un miembro k vacío en bytes vacíos. La construcción HMAC usa entonces esa clave vacía, cuya firma cualquiera puede calcular.
En jose/backends/native.py en la versión 3.5.0, HMACKey.__init__ acepta cualquier valor de tipo cadena o bytes:
def __init__(self, key, algorithm):
if algorithm not in ALGORITHMS.HMAC:
raise JWKError("hash_alg: %s is not a valid hash algorithm" % algorithm)
self._algorithm = algorithm
self._hash_alg = self.HASHES.get(algorithm)
if isinstance(key, dict):
self.prepared_key = self._process_jwk(key)
return
if not isinstance(key, str) and not isinstance(key, bytes):
raise JWKError("Expecting a string- or bytes-formatted key.")
if isinstance(key, str):
key = key.encode("utf-8")
if is_pem_format(key) or is_ssh_key(key):
raise JWKError(
"The specified key is an asymmetric key or x509 certificate and"
" should not be used as an HMAC secret."
)
self.prepared_key = key
No hay comprobación len(key) == 0. _process_jwk decodifica el miembro k con base64url_decode, y esa función devuelve una cadena de bytes vacía para una entrada vacía:
def _process_jwk(self, jwk_dict):
if not jwk_dict.get("kty") == "oct":
raise JWKError("Incorrect key type. Expected: 'oct', Received: %s" % jwk_dict.get("kty"))
k = jwk_dict.get("k")
k = k.encode("utf-8")
k = bytes(k)
k = base64url_decode(k)
return k
CryptographyHMACKey en jose/backends/cryptography_backend.py repite la misma lógica de constructor y tiene el mismo hueco. Ambos backends calculan después hmac.new(self.prepared_key, msg, self._hash_alg) y comparan el resultado con hmac.compare_digest o h.verify. Con una clave vacía, ese cálculo es determinista y público, así que la comparación tiene éxito para una firma producida por el atacante.
La ruta JWK hace que la falla sea alcanzable desde la configuración en lugar de desde una cadena vacía evidente. jws._get_keys() trata un mapping con kty como un JWK individual, jwk.construct() devuelve la clase de clave HMAC para un algoritmo HS, y el k vacío se convierte en b"" antes de la verificación.
jws._verify_signature() lee alg de la cabecera del token, construye la clave para cada candidato y llama a key.verify(). Con una clave vacía esa llamada devuelve verdadero para la firma del atacante, y el resto de la validación JWT continúa con las reclamaciones elegidas por el atacante.
¿Cómo se explota?
El atacante necesita un punto de conexión de tokens o cualquier petición que lleve un JWT, y saber o adivinar que la clave del despliegue está vacía:
- El atacante encuentra una aplicación que verifica tokens HS256 con python-jose y cuya clave se resuelve como vacía, por ejemplo porque
JWT_SECRETno está definido y el código pasaos.environ.get("JWT_SECRET", ""). - El atacante construye un token con las reclamaciones en las que confía la aplicación, como
{"sub": "admin", "role": "admin"}, y lo firma con una clave HMAC vacía. - El atacante envía el token. python-jose construye
HMACKey(b"", "HS256"), calcula la misma firma y acepta el token. - La aplicación actúa sobre las reclamaciones falsificadas, porque nunca ve un problema de clave.
La prueba de concepto del reportero es pública en el ticket #398 y se ejecuta tal cual:
import jose.jwt as jwt
# This succeeds and should raise an error.
token = jwt.encode({"admin": True}, "", algorithm="HS256")
jwt.decode(token, "", algorithms=["HS256"]) # Returns {"admin": True}
Lo mismo funciona con la forma JWK, que es la que usa un despliegue guiado por JWKS:
jwt.decode(token, {"kty": "oct", "k": ""}, algorithms=["HS256"])
¿Cómo se desarrolla el ataque?
Una aplicación que deja que su clave de verificación HMAC se resuelva como vacía entrega a python-jose un secreto de longitud cero, y python-jose firma y verifica con él en lugar de rechazar la configuración.
flowchart LR
A["La clave de la aplicación se resuelve vacía"] --> B["HMACKey acepta una clave de longitud cero"]
B --> C["El atacante calcula el HMAC con la clave vacía"]
C --> D["jwt.decode compara las firmas"]
D --> E["Reclamaciones falsificadas aceptadas"]
¿Qué puede hacer un atacante?
Con un token falsificado aceptado, el atacante asume todas las reclamaciones en las que confía la aplicación. Eso puede incluir:
- Cualquier identidad de usuario, incluido un administrador, cuando la aplicación autoriza a partir de las reclamaciones.
- Cualquier identificador de inquilino u organización que la aplicación use para delimitar datos.
- Una expiración prolongada, porque el atacante elige la reclamación
exp. - Cualquier permiso o rol que la aplicación lea del token.
El atacante no puede recuperar una clave de firma real, descifrar otros tokens ni firmar tokens que acepte un despliegue configurado correctamente. El impacto depende de lo que la aplicación haga con las reclamaciones y de si existe la condición de clave vacía.
Los tokens de servicio a servicio también importan. Un token falsificado para una API interna puede cruzar una frontera de confianza que nunca ve a un usuario. Una clave vacía en un servicio en segundo plano puede exponer por tanto más que un punto de conexión de inicio de sesión.
Cualquier petición que lleve un token bearer es candidata, incluidas las tareas de CI y las llamadas internas entre servicios. Un despliegue con un secreto fuerte y no vacío y una lista blanca de algoritmos explícita no es explotable por esta falla.
¿Por qué funciona la corrección?
Una clave HMAC de longitud cero no tiene secreto: cualquiera puede calcular el mismo MAC, así que un verificador que recibe una no autentica nada. La corrección es una comprobación de longitud en la aplicación o el paso a una biblioteca que la aplique.
La comprobación que debe añadir una versión corregida es pequeña. En ambos backends, inmediatamente después de la comprobación de tipo:
if not isinstance(key, str) and not isinstance(key, bytes):
raise JWKError("Expecting a string- or bytes-formatted key.")
+ if len(key) == 0:
+ raise JWKError("HMAC key must not be empty.")
+
if isinstance(key, str):
key = key.encode("utf-8")
La ruta JWK necesita su propia comprobación después de base64url_decode, porque un k vacío nunca llega a la rama de cadena.
PyJWT realiza la misma comprobación de longitud y se verificó localmente el 2026-11-09 con una codificación y una decodificación HS256. Eso hace de la migración una corrección de esta falla, no solo un cambio de biblioteca.
El ticket de python-jose enumera otros cuatro hallazgos en el mismo informe, incluidos la confusión de algoritmos con claves DER y el valor predeterminado algorithms=None. La migración corrige la falla de clave vacía y evita depender de un proyecto sin mantenimiento para los demás.
¿Cómo compruebo si estuve comprometido?
Un token falsificado parece un token válido, así que la comprobación de firma no puede decirle que fue falsificado. Busque primero la condición de configuración y después la actividad de cuenta que el usuario real no realizó.
- Confirme si algún despliegue puede pasar una clave vacía. Revise los valores predeterminados de configuración y el código de carga de claves, y consulte el punto de conexión JWKS que usa.
- Decodifique los tokens que su servicio aceptó durante la ventana de exposición y busque reclamaciones que no coincidan con la cuenta, como un
sub, rol o inquilino inesperado. La carga útil se puede leer sin la clave.
# Show a token's header and claims without verifying it.
python - <<'PY'
import base64, json, sys
token = sys.stdin.read().strip()
for part in token.split(".")[:2]:
part += "=" * (-len(part) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(part)), indent=2))
PY
- Busque en los registros de autenticación tokens con un
iat,exposubinusual para la cuenta. Después busque acciones administrativas de sesiones que no puedan vincularse a un inicio de sesión real. - Cambie la clave de verificación por un valor aleatorio fuerte e invalide las sesiones existentes. Si la clave estaba vacía, todo token firmado durante ese periodo debe tratarse como no confiable.
- Pase a PyJWT o añada la comprobación, y vuelva a probar que se rechaza una clave vacía.
Un intento de falsificación fallido puede no dejar rastro más allá de la propia petición, porque una firma rechazada y una falsificada siguen el mismo camino de código hasta la comparación.
Cronología
- 2025-05-28: se publica python-jose 3.5.0, la última versión al 2026-11-09.
- 2026-03-08: el reportero abre el ticket #398 con el hallazgo de clave vacía y una prueba de concepto.
- 2026-11-02: VulnCheck publica CVE-2026-120459.
Preguntas frecuentes
¿Mi aplicación está afectada si mi secreto HMAC es un valor aleatorio fuerte?
No. La falla exige que la clave de verificación se resuelva como un valor vacío. Un secreto fuerte y no vacío hace imposible el cálculo de falsificación para un atacante, incluso en una versión afectada de python-jose.
¿Pasar algorithms=["HS256"] corrige la falla de clave vacía?
No. La lista blanca limita los algoritmos que acepta el verificador, pero no comprueba la longitud de la clave. Un token firmado con HS256 y una clave vacía sigue verificándose cuando la aplicación pasa una clave vacía y permite HS256.
¿Existe una versión corregida de python-jose?
No. La última versión sigue siendo 3.5.0 del 2025-05-28, y el registro CVE no nombra ninguna versión corregida. Use la comprobación de la aplicación o migre a una biblioteca mantenida.
¿PyJWT tiene el mismo problema?
No. PyJWT 2.15.1 rechaza una clave HMAC vacía con InvalidKeyError("HMAC key must not be empty."). Eso se verificó localmente el 2026-11-09 con una codificación y decodificación HS256.
¿Ayuda cambiar la clave?
Sí, si cada despliegue recibe el nuevo valor. Cambiar a un secreto aleatorio fuerte elimina la condición de clave vacía de la que depende la falsificación, y también invalida los tokens firmados con la clave antigua. Una clave vacía sigue siendo explotable donde la configuración aún se resuelve como vacía, así que combine el cambio con la comprobación.
¿Puede un atacante explotar esto sin saber que la clave está vacía?
El atacante necesita que se cumpla la condición de clave vacía. Algunos despliegues la hacen adivinable, por ejemplo una variable de entorno no definida con un valor predeterminado vacío o una entrada JWKS con k: "". Una vez que se cumple, el atacante calcula la firma localmente y no necesita más acceso.
Encuentre servicios que usan python-jose con Ostorlab
El análisis de composición de software y el inventario SBOM de Ostorlab listan los paquetes Python y las versiones que resuelven sus servicios. Eso le permite encontrar las aplicaciones que aún dependen de python-jose y hacerlas pasar por la comprobación de clave vacía o una migración.
Referencias
- Aviso de VulnCheck, "python-jose through 3.5.0 Authentication Bypass via Empty HMAC Key" (2026-11-02): https://www.vulncheck.com/advisories/python-jose-through-3.5.0-authentication-bypass-via-empty-hmac-key (comprobado el 9 de noviembre de 2026)
- Registro CVE CVE-2026-120459 (2026-11-02): https://www.cve.org/CVERecord?id=CVE-2026-120459 (comprobado el 9 de noviembre de 2026)
- Ticket de python-jose #398, "Security: DER key algorithm confusion, empty HMAC key, and timing side-channels" (2026-03-08): https://github.com/mpdavis/python-jose/issues/398 (comprobado el 9 de noviembre de 2026)
- python-jose 3.5.0,
jose/backends/native.py(2025-05-28): https://github.com/mpdavis/python-jose/blob/3.5.0/jose/backends/native.py (comprobado el 9 de noviembre de 2026) - python-jose 3.5.0,
jose/backends/cryptography_backend.py(2025-05-28): https://github.com/mpdavis/python-jose/blob/3.5.0/jose/backends/cryptography_backend.py (comprobado el 9 de noviembre de 2026) - PyJWT 2.15.1,
HMACAlgorithm.prepare_key(comprobado el 9 de noviembre de 2026): https://github.com/jpadilla/pyjwt/blob/2.15.1/jwt/algorithms.py - Página de PyPI de python-jose, historial de versiones (comprobado el 9 de noviembre de 2026): https://pypi.org/project/python-jose/
- pypistats, descargas semanales de python-jose (comprobado el 9 de noviembre de 2026): https://pypistats.org/packages/python-jose