CVE-2026-39999: Verified Reproduction
CVE-2026-39999: Apache APISIX jwt-auth authentication bypass via algorithm confusion
CVE-2026-39999 is verified against apache/apisix · github. Affected versions: 2.2.0 through 3.16.0. Fixed in 3.17.0. Vulnerability class: Auth Bypass. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00287.
What Is CVE-2026-39999?
CVE-2026-39999 is a critical authentication bypass (CWE-290) in Apache APISIX's jwt-auth plugin caused by JWT algorithm confusion. An attacker who knows a consumer's RSA public key can forge a valid token and bypass authentication entirely. Pruva reproduced it (reproduction REPRO-2026-00287).
CVE-2026-39999 Severity & CVSS Score
CVE-2026-39999 is rated critical severity, with a CVSS base score of 9.1 out of 10.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
Affected apache/apisix Versions
apache/apisix · github versions 2.2.0 through 3.16.0 are affected.
How to Reproduce CVE-2026-39999
pruva-verify REPRO-2026-00287 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00287/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-39999
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
JWT header alg value set to HS256 and HMAC-SHA256 signature computed using the consumer's RSA public key (PEM) as the HMAC secret
- HTTP GET / to APISIX gateway (port 9080) with Authorization: Bearer <forged HS256 JWT> against a route protected by the jwt-auth plugin with an RS256 consumer configuration
How the agent worked
Root Cause and Exploit Chain for CVE-2026-39999
Apache APISIX's jwt-auth plugin selected the JWT signature verification primitive
from the attacker-controlled JWT header alg field, while independently selecting
the verification key from trusted consumer configuration. When a consumer was
configured with an RS256 (asymmetric) algorithm, the plugin used the consumer's RSA
public key as the verification key. An attacker who knew that public key could
forge a JWT whose header declared alg=HS256 and whose HMAC-SHA256 signature was
computed over the token using the public key PEM as the HMAC secret. The plugin then
verified the forged token with HMAC-SHA256 — using the same public key as the secret —
and accepted it, bypassing authentication entirely. This is the classic JWT algorithm
confusion attack (CVE-2015-9235 pattern) realized in APISIX's jwt-auth verifier.
- Package/component affected:
apisix/plugins/jwt-auth(thejwt-authauthentication plugin), specifically thefind_consumer()→get_auth_secret()→verify_signature()path inapisix/plugins/jwt-auth.luaandapisix/plugins/jwt-auth/parser.lua. - Affected versions: Apache APISIX 2.2.0 through 3.16.0 (all versions that supported
asymmetric algorithms in
jwt-authand lacked the algorithm-match enforcement). - Fixed in: 3.17.0 (commit
e4de423fc583fdf82f47a1dda6c666aa2f2b3dca— "fix(jwt-auth): enforce algorithm match before signature verification (#13182)"). - Risk level: Critical. An unauthenticated remote attacker who can obtain a
consumer's configured public key (which is, by definition, public) can forge a
valid-looking JWT and access any route protected by that consumer's
jwt-authconfiguration. No private key material is required.
Impact Parity
- Disclosed/claimed maximum impact: Authentication bypass / authorization bypass
(
authz_bypass) on remote API requests authenticated viajwt-authwith an RS256-style consumer. - Reproduced impact from this run: Full authentication bypass demonstrated
end-to-end against a running Apache APISIX 3.16.0 gateway. A forged HS256 JWT
(signed with the consumer's RSA public key as the HMAC secret) was accepted by the
protected route (HTTP 200, request proxied to upstream), while the identical
forged token was rejected by the fixed 3.17.0 gateway (HTTP 401,
"failed to verify jwt: algorithm mismatch, expected RS256"). - Parity: full. The remote API authentication bypass described in the advisory was reproduced exactly, and the negative control on the fixed version confirms the fix closes the bypass.
- Not demonstrated: N/A — the claim is authorization bypass, not code execution, and the bypass itself was demonstrated in full.
Root Cause
In the vulnerable find_consumer() function (apisix/plugins/jwt-auth.lua, 3.16.0):
- The JWT is parsed and the consumer is located by the
keyclaim in the payload (consumer_mod.find_consumer(plugin_name, "key", user_key)). get_auth_secret(consumer)returns the verification key based on the consumer's configured algorithm:
For an RS256 consumer, the returned key is the RSA public key.local function get_auth_secret(consumer) if not consumer.auth_conf.algorithm or consumer.auth_conf.algorithm:sub(1, 2) == "HS" then return get_secret(consumer.auth_conf) -- HMAC secret else return consumer.auth_conf.public_key -- RSA public key (for RS256) end endjwt:verify_signature(auth_secret)inparser.luaselects the verification primitive fromself.header.alg— the JWT header, which is attacker-controlled:function _M.verify_signature(self, key) return alg_verify[self.header.alg](self.raw_header .. "." .. self.raw_payload, base64_decode(self.signature), key) end- There was no check that
self.header.algmatchedconsumer.auth_conf.algorithm. So when the attacker setsalg=HS256,alg_verify["HS256"](data, signature, public_key)is invoked, which computesHMAC-SHA256(data, public_key)and compares it to the attacker-supplied signature. Because the public key is public, the attacker can compute the identical HMAC and produce a matching signature → authentication bypass.
The fix (commit e4de423f, released in 3.17.0) inserts an algorithm-match
enforcement before verify_signature:
-- Enforce that the JWT header's "alg" matches the consumer's configured algorithm
local expected_alg = consumer.auth_conf.algorithm or "HS256"
local token_alg = jwt.header and jwt.header.alg
if token_alg ~= expected_alg then
local err = "failed to verify jwt: algorithm mismatch, expected " .. expected_alg
...
return nil, nil, "failed to verify jwt"
end
This rejects any token whose header alg differs from the consumer's configured
algorithm (e.g. an HS256 token against an RS256 consumer) before signature
verification, closing the confusion.
Reproduction Steps
- Script:
bundle/repro/reproduction_steps.sh(self-contained, idempotent). - What it does:
- Generates a fresh RSA-2048 keypair with
openssl. - Deploys the real Apache APISIX gateway inside Docker containers on an
isolated bridge network:
bitnamilegacy/etcd:3.6(config store),python:3-slim(upstream HTTP backend + client helper), andapache/apisix:3.16.0-debian(vulnerable) thenapache/apisix:3.17.0-debian(fixed). - Uses the APISIX Admin API to create a consumer (
jack) with thejwt-authplugin configured for RS256 using the generated RSA public key, and a route (/) protected byjwt-authwith the upstream backend. - Verifies the route is genuinely protected: a request without a JWT returns HTTP 401.
- Forges a JWT with header
{"typ":"JWT","alg":"HS256"}and payload{"key":"jack-key"}, signing theheader.payloadinput withHMAC-SHA256(..., RSA_public_key_PEM)— using the consumer's public key as the HMAC secret. - Sends the forged token to the protected route via
Authorization: Bearer <jwt>. - On the vulnerable 3.16.0 gateway: the request is accepted (HTTP 200, proxied to upstream) → authentication bypass.
- On the fixed 3.17.0 gateway (fresh etcd, same consumer/route): the identical
forged token is rejected (HTTP 401,
"failed to verify jwt") with the logalgorithm mismatch, expected RS256→ fix confirmed.
- Generates a fresh RSA-2048 keypair with
- Expected evidence: Two consecutive runs both show vulnerable→200 (bypass) and
fixed→401 (reject); APISIX error logs on 3.17.0 contain the
jwt-auth.lua:338: ... algorithm mismatch, expected RS256line.
Evidence
bundle/logs/harness_vulnerable.log— exploit harness output on 3.16.0 (forged HS256 JWT → HTTP 200,[+] VULNERABLE: forged HS256 JWT accepted -> authentication BYPASS).bundle/logs/harness_fixed.log— exploit harness output on 3.17.0 (forged HS256 JWT → HTTP 401,[+] FIXED: forged HS256 JWT rejected).bundle/logs/apisix_fixed.log— APISIX 3.17.0 nginx error log containing:[lua] jwt-auth.lua:338: find_consumer(): failed to verify jwt: algorithm mismatch, expected RS256.bundle/artifacts/results_vulnerable.json—{"forged_jwt_code": 200, "no_jwt_code": 401, ...}.bundle/artifacts/results_fixed.json—{"forged_jwt_code": 401, "no_jwt_code": 401, "forged_jwt_body": "{\"message\":\"failed to verify jwt\"}"}.bundle/artifacts/rsa_public.pem/rsa_private.pem— the RSA keypair used.bundle/artifacts/apisix_config.yaml— the APISIX deployment config (etcd + admin key).bundle/repro/runtime_manifest.json— structured runtime evidence manifest.
Key excerpt (vulnerable, 3.16.0):
[*] no-jwt: 401 (expect 401)
[*] forged HS256 JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJrZXkiOiJqYWNrLWtleSJ9...
[*] forged-jwt: 200 body=<!DOCTYPE HTML>...
[+] VULNERABLE: forged HS256 JWT accepted -> authentication BYPASS
Key excerpt (fixed, 3.17.0):
[*] forged-jwt: 401 body={"message":"failed to verify jwt"}
[+] FIXED: forged HS256 JWT rejected (401) -> algorithm mismatch enforced
Environment: Docker (Apache APISIX official images), isolated bridge network,
OpenResty/LuaJIT runtime inside the apache/apisix:*-debian images; APISIX versions
verified via apisix version (3.16.0 / 3.17.0).
Recommendations / Next Steps
- Upgrade to Apache APISIX 3.17.0 or later, which enforces that the JWT header
algmatches the consumer's configured algorithm before signature verification. - Defense in depth: Consider also key-type/algorithm binding at the verifier primitive level (e.g. refuse to use an asymmetric key material with an HMAC primitive, and vice versa), so that a single missing upper-layer check cannot reintroduce the confusion.
- Audit existing
jwt-authconsumers: any consumer configured with an asymmetric algorithm (RS*/ES*/PS*/EdDSA) whose public key is exposed was exploitable prior to the fix; rotate keys and review access logs for forged-token usage. - Testing: Add a regression test that submits an HS256-forged token against an
RS256 consumer and asserts a 401 (the fix commit
e4de423falready added such tests int/plugin/jwt-auth.t).
Additional Notes
- Idempotency: The script cleans up all containers/networks on exit and was run twice consecutively with identical results (vulnerable→200, fixed→401) — reproducibility confirmed.
- The forged JWT contains only the
keyclaim (matching the consumer) and omitsexp/nbf; on 3.16.0verify_claimsonly validates those claims when present, so the token passes claim validation. This keeps the proof focused on the algorithm confusion and avoids confounding it with expiry behavior. - The attack requires only the consumer's public key (which is public by design for RS256); no private key, no secrets, and no prior authentication are needed.
- Networking note: the sandbox runs Docker-in-Docker where host port publishing is not
directly reachable from the shell; the script therefore drives all HTTP traffic
through a
python:3-slimclient container on the same bridge network, addressing the APISIX gateway by its container alias.
CVE-2026-39999 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
ls -la /data/pruva/project-cache/0a14f1a6-2023-402d-a83c-902eda6963c4/ 2>/dev/null && echo "---REPO---" && ls -la /data/pruva/project-cache/0a14f1a6-2023-402d-a83c-902eda6963c4/repo 2>/dev/null | head -30total 0 drwxr-xr-x 1 vscode vscode 24 Jul 13 16:28 . drwxr-xr-x 1 vscode vscode 6790 Jul 13 16:28 .. drwxr-xr-x 1 vscode vscode 0 Jul 13 16:28 repo-mirrors ---REPO---
Artifacts and Evidence for CVE-2026-39999
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-39999
Upgrade apache/apisix · github to 3.17.0 or later.
FAQ: CVE-2026-39999
How does the CVE-2026-39999 algorithm-confusion attack work?
Which APISIX versions are affected by CVE-2026-39999, and where is it fixed?
How severe is CVE-2026-39999?
How can I reproduce CVE-2026-39999?
References for CVE-2026-39999
Authoritative sources for CVE-2026-39999 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.