CVE-2026-39999: Verified Repro With Script Download
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://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
Bypass/variant probe for CVE-2026-39999 (Apache APISIX jwt-auth JWT algorithm confusion). Tested whether the 3.17.0 alg-match fix can be bypassed, and whether the same confusion reaches consumers configured with asymmetric algorithms other than RS256 (ES256/ECDSA, EdDSA/Ed25519). Result: NO BYPASS. The fix rejects for…
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.
Variant Analysis & Alternative Triggers for CVE-2026-39999
No bypass of the 3.17.0 fix was found, and no materially-distinct entry point to
the vulnerable sink exists. The fix (commit e4de423f, "enforce algorithm match
before signature verification") inserts a single guard in find_consumer that
compares the JWT header alg to the consumer's configured algorithm before
verify_signature. Because that check and verify_signature both read the same
self.header.alg field, no token can simultaneously pass the check and dispatch
to a mismatched (HMAC) primitive. The guard sits on the only path to the sink
(verify_signature is called only from find_consumer), and is generic over all
supported consumer algorithms. Runtime testing confirmed that the same forged-HS256
attack that bypasses RS256 consumers on 3.16.0 also bypasses ES256 (ECDSA)
and EdDSA (Ed25519) consumers on 3.16.0 (alternate triggers, same root cause),
and that the fix rejects all three families on 3.17.0 with explicit
algorithm mismatch, expected <alg> log lines. These alternate triggers share the
identical sink, entry point and trust boundary as the original CVE and are covered
by the generic fix — they are not distinct variants under the strict definition and
do not constitute a bypass.
Fix Coverage / Assumptions
Invariant the fix relies on: the JWT header field compared in the new guard
(jwt.header.alg) is the same field that parser.lua's verify_signature
dispatches on (alg_verify[self.header.alg]). Both are populated once by
resty.jwt's load_jwt/parse_jwt as cjson.decode(base64url_decode(raw_header)).alg
with no normalization, so the compared value and the dispatched value can never
diverge.
Code paths explicitly covered: find_consumer is the sole caller of
jwt:verify_signature (verified by source-wide search), and find_consumer is
called only from _M.rewrite. The guard therefore covers every runtime path:
header / query-string / cookie token delivery, standalone jwt-auth, jwt-auth under
multi-auth, and all consumer algorithms in the schema enum (HS*, RS*, ES*, PS*,
EdDSA) — because the check compares the generic consumer.auth_conf.algorithm
string rather than a hardcoded value.
What the fix does NOT cover: no gap was found. Candidate bypass paths were
evaluated and ruled out: (a) parsing quirks — load_jwt does no alg
normalization/whitelist, but the check and dispatch share the same field so no
value can pass the check and trigger HMAC; (b) an alternate entry point to the
sink — none exists (verify_signature has one caller); (c) key-selection
divergence — get_auth_secret keys off the consumer's algorithm, which the fix
forces equal to the token's; (d) anonymous-consumer / multi-auth fallback — by
design, grants only the configured anonymous privileges (same as no token). The
only residual item is a defense-in-depth recommendation (bind key type to
primitive type inside alg_verify), not an exploitable gap. See
bundle/vuln_variant/patch_analysis.md for the full analysis.
Variant / Alternate Trigger
Bypass attempted: forge an HS256 JWT and submit it against a consumer configured with an asymmetric algorithm other than RS256 — specifically ES256 (ECDSA / prime256v1) and EdDSA (Ed25519) — signing the token with that consumer's public key PEM as the HMAC secret. If the fix only handled RS256 (or hardcoded an RS256 expectation), these would slip through on 3.17.0.
Entry point (identical for all candidates): HTTP GET / to the APISIX gateway
(port 9080) with Authorization: Bearer <forged JWT>, against a route protected by
the jwt-auth plugin. The forged token's payload key claim selects the targeted
consumer (and thus the public key used as the HMAC secret).
Code path involved (same for every candidate):
apisix/plugins/jwt-auth.lua→find_consumer:fetch_jwt_token→jwt_parser.new→consumer_mod.find_consumer("key", …)→get_auth_secret(returnsconsumer.auth_conf.public_keyfor any non-HS algorithm) → [fix guard]jwt.header.alg ~= consumer.auth_conf.algorithm→jwt:verify_signature(auth_secret).apisix/plugins/jwt-auth/parser.lua→_M.verify_signature:alg_verify[self.header.alg](raw_header.."."..raw_payload, sig, key). For the forged tokenself.header.alg == "HS256", soalg_verify["HS256"]computesHMAC-SHA256(data, public_key_PEM)and compares it to the attacker-supplied signature.
Tested variants (all same root cause / sink / entry point / trust boundary):
- RS256 consumer + HS256 forged token — original CVE (control). 3.16.0: 200; 3.17.0: 401.
- ES256 consumer + HS256 forged token — EC public key as HMAC secret. 3.16.0: 200; 3.17.0: 401.
- EdDSA consumer + HS256 forged token — Ed25519 public key as HMAC secret. 3.16.0: 200; 3.17.0: 401.
Candidates #2 and #3 are alternate triggers (they prove the confusion is not
RS256-specific) but are not distinct variants: the entry point, sink
(alg_verify["HS256"] with the consumer public key), trust boundary (remote
untrusted caller → authenticated route) and technique (forge HS256, sign with the
public key PEM) are identical to the original CVE; only the consumer's configured
algorithm and key material differ. Per the variant guidance, relabeling the same
sink via a different consumer configuration is the same bug, not a new variant.
Critically, none reproduce on the fixed version, so there is no bypass.
- Package/component affected:
apisix/plugins/jwt-auth(find_consumer,get_auth_secret) andapisix/plugins/jwt-auth/parser.lua(verify_signature). - Affected versions (as tested): Apache APISIX 3.16.0 (vulnerable — confirmed bypass for RS256, ES256, EdDSA); Apache APISIX 3.17.0 (fixed — all rejected). Advisory scope: 2.2.0–3.16.0; fixed in 3.17.0.
- Risk level: Critical for the vulnerable version (unauthenticated remote authentication bypass). For the variant stage's bypass question: none — the fix holds, so there is no residual risk from the tested paths on 3.17.0.
Impact Parity
- Disclosed/claimed maximum impact (parent CVE): authentication/authorization
bypass (
authz_bypass) on remote API requests authenticated viajwt-authwith an asymmetric consumer config. - Reproduced impact from this variant run: On 3.16.0 the alternate triggers (ES256, EdDSA) yield the same full auth bypass (HTTP 200, request proxied to upstream) as the original RS256 bypass — confirming the vulnerability's breadth across asymmetric key families. On 3.17.0 all forged tokens are rejected (401).
- Parity (bypass question):
none— no bypass reproduced on the fixed version. - Parity (alternate-trigger question on vulnerable):
full— the same authz_bypass impact as the parent is reproduced for ES256 and EdDSA consumers on the vulnerable version. - Not demonstrated: N/A — the claim is authorization bypass, not code execution; the (non-)bypass and the alternate triggers were both demonstrated directly.
Root Cause
The underlying bug: jwt-auth selects the verification key from trusted
consumer configuration (get_auth_secret → consumer's public_key for asymmetric
algorithms) while selecting the verification primitive from the
attacker-controlled JWT header alg (alg_verify[self.header.alg]). When the two
disagree (token alg=HS256, consumer algorithm=RS256/ES256/EdDSA), the
consumer's public key is fed to HMAC-SHA256 as the secret. Because the public key
is public, the attacker can compute the identical HMAC and forge a valid token.
The fix enforces jwt.header.alg == consumer.auth_conf.algorithm before
verify_signature. Since the same self.header.alg is both checked and
dispatched, equality forces a consistent key/primitive pairing, eliminating the
confusion. This is correct and complete: there is no value of header.alg that
both satisfies the equality and selects HMAC against an asymmetric key. The same
logic is why the ES256 and EdDSA alternate triggers — which rely on the same
alg_verify["HS256"] dispatch with a different public key — are also blocked.
Fix commit: e4de423fc583fdf82f47a1dda6c666aa2f2b3dca (PR #13182, in 3.17.0).
Reproduction Steps
- Script:
bundle/vuln_variant/reproduction_steps.sh(self-contained, idempotent, Docker-based). - What it does:
- Generates three keypairs with
openssl: RSA-2048 (RS256), EC prime256v1 (ES256), Ed25519 (EdDSA). - Deploys the real Apache APISIX gateway (3.16.0 then 3.17.0) in Docker on an isolated bridge network with etcd + a python http.server upstream + a client helper.
- Via the Admin API, creates three consumers (
jack-rsRS256,jack-ecES256,jack-edEdDSA), each with a distinctkeyclaim and its respective public key, plus one route/protected byjwt-auth(the consumer is selected by the token'skeyclaim). - Verifies the route is protected (no JWT → 401 on both versions).
- For each consumer, forges an HS256 JWT (
{"typ":"JWT","alg":"HS256"}/ payload{"key":"<that consumer's key>"}) signed withHMAC-SHA256(signing_input, <consumer public key PEM>)and sends it to/. - On 3.16.0: asserts each forged token is accepted (HTTP 200 = bypass).
- On 3.17.0 (fresh etcd, same config): asserts each forged token is rejected (HTTP 401).
- Exit 0 = a forged token accepted on the fixed 3.17.0 (true bypass); exit 1 = no bypass (alternate triggers reproduced on vulnerable only).
- Generates three keypairs with
- Expected evidence: vulnerable harness shows RS256/ES256/EdDSA all
200 [BYPASS]; fixed harness shows all three401 [REJECTED]; fixed APISIX error log containsalgorithm mismatch, expected RS256,expected ES256, andexpected EdDSAatjwt-auth.lua:338. Script exits 1 (no bypass).
Evidence
bundle/logs/variant_harness_vulnerable.log— 3.16.0: RS256/ES256/EdDSA forged HS256 → 200 [BYPASS] each;[+] VULNERABLE: all three asymmetric consumers bypassed via HS256 confusion.bundle/logs/variant_harness_fixed.log— 3.17.0: RS256/ES256/EdDSA forged HS256 → 401 [REJECTED] each;[+] FIXED: all forged HS256 tokens rejected (401) -> fix covers RS256/ES256/EdDSA.bundle/logs/variant_apisix_fixed.log— APISIX 3.17.0 nginx error log with the three definitive lines (all fromjwt-auth.lua:338):failed to verify jwt: algorithm mismatch, expected RS256failed to verify jwt: algorithm mismatch, expected ES256failed to verify jwt: algorithm mismatch, expected EdDSA
bundle/logs/variant_apisix_vulnerable.log— 3.16.0 startup/plugin-load log (no alg-mismatch lines — the guard does not exist in 3.16.0).bundle/vuln_variant/artifacts/variant_results_vulnerable.json— per-algforged_jwt_code: 200, bypass: truefor RS256/ES256/EdDSA.bundle/vuln_variant/artifacts/variant_results_fixed.json— per-algforged_jwt_code: 401, bypass: falsefor RS256/ES256/EdDSA (with forged token strings).bundle/vuln_variant/artifacts/{rsa,ec,ed}_public.pem— the public keys used as HMAC secrets.bundle/vuln_variant/artifacts/variant_harness.py— the test harness.bundle/vuln_variant/logs/{vulnerable,fixed}_version.txt—apisix versionoutput (3.16.0 / 3.17.0).bundle/logs/variant_run1.stdout.log,bundle/logs/variant_run2.stdout.log— two consecutive full runs (idempotency).
Key excerpt (vulnerable, 3.16.0):
[*] no-jwt control: 401 (expect 401)
[*] RS256 forged HS256 -> 200 [BYPASS] body=<!DOCTYPE HTML>...
[*] ES256 forged HS256 -> 200 [BYPASS] body=<!DOCTYPE HTML>...
[*] EdDSA forged HS256 -> 200 [BYPASS] body=<!DOCTYPE HTML>...
[+] VULNERABLE: all three asymmetric consumers bypassed via HS256 confusion
Key excerpt (fixed, 3.17.0):
[*] RS256 forged HS256 -> 401 [REJECTED] body={"message":"failed to verify jwt"}
[*] ES256 forged HS256 -> 401 [REJECTED] body={"message":"failed to verify jwt"}
[*] EdDSA forged HS256 -> 401 [REJECTED] body={"message":"failed to verify jwt"}
[+] FIXED: all forged HS256 tokens rejected (401) -> fix covers RS256/ES256/EdDSA
Environment: Docker (official apache/apisix:3.16.0-debian and
apache/apisix:3.17.0-debian images), isolated bridge network,
OpenResty/LuaJIT runtime inside the images; apisix version reported 3.16.0 and
3.17.0 respectively. Image digests:
3.16.0-debian@sha256:b921bdef…, 3.17.0-debian@sha256:6cbf65f3….
Recommendations / Next Steps
- The fix needs no extension for the algorithm-confusion class — it is complete and covers all consumer algorithms. No code change is required to close a bypass (none exists).
- Defense in depth (recommended, not required): bind key type to primitive
type inside
parser.lua'salg_verify/alg_sign— e.g. refuse to construct an HMAC primitive when the supplied key parses as an asymmetric PEM (and vice-versa). This makes the confusion structurally impossible at the verifier layer, so a future regression that adds a second caller ofverify_signature(or bypassesfind_consumer) cannot reintroduce the bug. The repro RCA made the same recommendation. - Regression test coverage: the fix's TEST 52 covers RS256. Add parallel cases
for ES256 and EdDSA (forged HS256 → 401 with
expected ES256/expected EdDSA) to pin the generic coverage demonstrated in this run. - Consumer audit: any pre-3.17.0 consumer configured with an asymmetric algorithm (RS*/ES*/PS*/EdDSA) whose public key was exposed was exploitable; rotate keys and review access logs. This applies to all asymmetric families, not just RS256.
Additional Notes
- Idempotency: the script was run twice consecutively with identical results (run1 and run2 both: vulnerable → 200×3, fixed → 401×3, exit 1). It cleans up all containers/network on exit via a trap.
- Bounded search: fewer than three materially distinct variants exist because
verify_signaturehas exactly one caller (find_consumer, which has the fix) and the check/dispatch share the sameheader.alg. The three runtime candidates tested (RS256/ES256/EdDSA) are the same root cause/surface/entry point and differ only in consumer key material; HS384/HS512 HMAC-variant forgeries and query/cookie delivery channels are likewise the same sink and were ruled out by source analysis. Additional attempts would re-exercise the identicalalg_verify["HS*"]sink, not a distinct path. - Why this is a negative (no-bypass) verdict, not a confirmed variant: the ES256/EdDSA results reproduce only on the vulnerable version and reach the identical sink via the identical entry point/trust boundary as the parent CVE. They confirm the fix's breadth, not a gap. Claiming them as a "distinct variant" would be relabeling the same behavior through a different consumer configuration, which the variant guidance explicitly excludes.
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.