Skip to content

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.

REPRO-2026-00287 apache/apisix · github Auth Bypass Jul 14, 2026 CVE entry .txt
Severity
CRITICAL
CVSS
9.1
Confidence
HIGH
Reproduced in
14m 33s
Tool calls
183
Spend
$2.62
01 · Overview

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).

02 · Severity & CVSS

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 threat level
9.1 / 10 CVSS base
Weakness CWE-290 Authentication Bypass by Spoofing — Authentication Bypass by Spoofing

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

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
or 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
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-39999

Authorization bypass — reproduced
  • 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
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

Attack chain
  1. 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 443 events · 183 tool calls · 15 min
15 minDuration
183Tool calls
121Reasoning steps
443Events
7Dead-ends
Agent activity over 15 min
Support
21
Repro
218
Judge
20
Variant
180
0:0014:33

Root Cause and Exploit Chain for CVE-2026-39999

Versions: Apache APISIX 2.2.0 through 3.16.0 (all versions that supportedFixed: in: 3.17.0 (commit e4de423fc583fdf82f47a1dda6c666aa2f2b3dca —

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 (the jwt-auth authentication plugin), specifically the find_consumer()get_auth_secret()verify_signature() path in apisix/plugins/jwt-auth.lua and apisix/plugins/jwt-auth/parser.lua.
  • Affected versions: Apache APISIX 2.2.0 through 3.16.0 (all versions that supported asymmetric algorithms in jwt-auth and 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-auth configuration. No private key material is required.

Impact Parity

  • Disclosed/claimed maximum impact: Authentication bypass / authorization bypass (authz_bypass) on remote API requests authenticated via jwt-auth with 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):

  1. The JWT is parsed and the consumer is located by the key claim in the payload (consumer_mod.find_consumer(plugin_name, "key", user_key)).
  2. get_auth_secret(consumer) returns the verification key based on the consumer's configured algorithm:
    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
    end
    
    For an RS256 consumer, the returned key is the RSA public key.
  3. jwt:verify_signature(auth_secret) in parser.lua selects the verification primitive from self.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
    
  4. There was no check that self.header.alg matched consumer.auth_conf.algorithm. So when the attacker sets alg=HS256, alg_verify["HS256"](data, signature, public_key) is invoked, which computes HMAC-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

  1. Script: bundle/repro/reproduction_steps.sh (self-contained, idempotent).
  2. 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), and apache/apisix:3.16.0-debian (vulnerable) then apache/apisix:3.17.0-debian (fixed).
    • Uses the APISIX Admin API to create a consumer (jack) with the jwt-auth plugin configured for RS256 using the generated RSA public key, and a route (/) protected by jwt-auth with 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 the header.payload input with HMAC-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 log algorithm mismatch, expected RS256 → fix confirmed.
  3. 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 RS256 line.

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 alg matches 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-auth consumers: 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 e4de423f already added such tests in t/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 key claim (matching the consumer) and omits exp/nbf; on 3.16.0 verify_claims only 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-slim client 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.

Event 1/40
0:000:53
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-39999 · REPRO-20
0:03
0:04
web search
0:05
web search
0:07
0:08
0:10
web search
0:12
0:18
web search
0:21
0:23
0:24
web search
0:41
0:42
extract_facts
no facts extracted
0:43
0:43
0:43
supportrepro
0:44
0:44
0:44
0:45
0:45
0:45
0:46
0:46
0:49
0:49
0:49
0:49
$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 -30
total 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---
0:52
08 · How to Fix

How to Fix CVE-2026-39999

Upgrade apache/apisix · github to 3.17.0 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-39999 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-39999

How does the CVE-2026-39999 algorithm-confusion attack work?

An attacker who knows the RS256 consumer's public key forges a JWT with header alg=HS256 and an HMAC-SHA256 signature computed over the token using the public-key PEM as the HMAC secret. APISIX then verifies with HMAC-SHA256 using that same public key as the secret and accepts the forged token — the classic JWT algorithm-confusion (CVE-2015-9235) pattern realized in jwt-auth.

Which APISIX versions are affected by CVE-2026-39999, and where is it fixed?

Apache APISIX versions 2.2.0 through 3.16.0 are affected in the jwt-auth plugin. It is fixed in 3.17.0 — upgrade to 3.17.0 or later.

How severe is CVE-2026-39999?

It is rated critical severity: a complete authentication bypass against consumers configured with asymmetric (RS256-style) keys, requiring only knowledge of the public key.

How can I reproduce CVE-2026-39999?

Download the verified script from this page and run it in an isolated environment against APISIX 2.2.0-3.16.0 with an RS256 jwt-auth consumer. It forges an HS256 token signed with the public-key PEM and shows APISIX accepting it, then confirms 3.17.0 rejects it.
11 · References

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.