{
  "parent_cve": "CVE-2026-39999",
  "parent_root_cause": "apisix/plugins/jwt-auth.lua find_consumer selects the verification key from the consumer's configured algorithm (get_auth_secret -> consumer.public_key for asymmetric algs) while apisix/plugins/jwt-auth/parser.lua verify_signature selects the verification primitive from the attacker-controlled JWT header alg (alg_verify[self.header.alg]). When token alg=HS256 is used against an asymmetric consumer, the consumer's public key is fed to HMAC-SHA256 as the secret, enabling a forged-token authentication bypass (classic JWT algorithm confusion, CVE-2015-9235 pattern).",
  "parent_sink": "apisix/plugins/jwt-auth/parser.lua:225-228 _M.verify_signature -> alg_verify[self.header.alg](raw_header..raw_payload, base64_decode(signature), key)",
  "parent_entrypoint": "HTTP request to an APISIX route protected by jwt-auth (header/query/cookie token delivery); consumer selected by token key claim",
  "parent_trust_boundary": "remote unauthenticated downstream caller -> authenticated gateway route",
  "variant_candidates": [
    {
      "id": "V2_es256",
      "candidate_description": "ES256 (ECDSA prime256v1) consumer + forged HS256 token signed with EC public key PEM as HMAC secret",
      "reaches_same_sink": true,
      "same_sink": "alg_verify[\"HS256\"](data, signature, consumer_public_key_pem)",
      "same_entrypoint": true,
      "same_trust_boundary": true,
      "same_root_cause": true,
      "root_cause_equivalence": "identical",
      "materially_distinct": false,
      "rationale": "Identical technique (forge HS256, sign with consumer public key as HMAC secret) reaching the identical alg_verify[\"HS256\"] sink via the identical entry point and trust boundary. Only the consumer's configured algorithm and public key material (EC PEM vs RSA PEM) differ. Relabeling the same sink via a different consumer configuration is not a distinct variant.",
      "vulnerable_3_16_0": "bypass (HTTP 200)",
      "fixed_3_17_0": "rejected (HTTP 401, 'algorithm mismatch, expected ES256')"
    },
    {
      "id": "V3_eddsa",
      "candidate_description": "EdDSA (Ed25519) consumer + forged HS256 token signed with Ed25519 public key PEM as HMAC secret",
      "reaches_same_sink": true,
      "same_sink": "alg_verify[\"HS256\"](data, signature, consumer_public_key_pem)",
      "same_entrypoint": true,
      "same_trust_boundary": true,
      "same_root_cause": true,
      "root_cause_equivalence": "identical",
      "materially_distinct": false,
      "rationale": "Same as V2: identical sink/entrypoint/trust boundary/technique; only the key material (Ed25519 PEM) differs. Not a distinct variant.",
      "vulnerable_3_16_0": "bypass (HTTP 200)",
      "fixed_3_17_0": "rejected (HTTP 401, 'algorithm mismatch, expected EdDSA')"
    },
    {
      "id": "R1_hs384_hs512",
      "candidate_description": "RS256 consumer + forged HS384/HS512 token signed with RSA public key as HMAC-SHA384/512 secret",
      "reaches_same_sink": true,
      "same_sink": "alg_verify[\"HS384\"|\"HS512\"](data, signature, consumer_public_key_pem)",
      "same_entrypoint": true,
      "same_trust_boundary": true,
      "same_root_cause": true,
      "root_cause_equivalence": "identical",
      "materially_distinct": false,
      "rationale": "Same confusion via a different HMAC variant (SHA-384/512 instead of SHA-256). Same sink family, same entry point. Ruled out by source analysis: the fix compares jwt.header.alg to consumer.auth_conf.algorithm, so HS384/HS512 ~= RS256 -> rejected on fixed.",
      "vulnerable_3_16_0": "expected bypass (not runtime-tested; alg_verify has HS384/HS512 entries)",
      "fixed_3_17_0": "rejected (alg mismatch)"
    },
    {
      "id": "R2_query_cookie",
      "candidate_description": "Forged HS256 token delivered via query string (?jwt=) or cookie instead of Authorization header",
      "reaches_same_sink": true,
      "same_sink": "alg_verify[\"HS256\"] via find_consumer (fetch_jwt_token reads header/query/cookie, then same path)",
      "same_entrypoint": true,
      "same_trust_boundary": true,
      "same_root_cause": true,
      "root_cause_equivalence": "identical",
      "materially_distinct": false,
      "rationale": "fetch_jwt_token supports header/query/cookie but all converge on the same find_consumer -> verify_signature path with the fix. Different input channel, same sink/entry point/trust boundary. Not a distinct variant.",
      "vulnerable_3_16_0": "bypass (same as header)",
      "fixed_3_17_0": "rejected (alg mismatch)"
    },
    {
      "id": "R3_alt_entrypoint",
      "candidate_description": "Alternate plugin/phase that calls verify_signature without the alg-match guard",
      "reaches_same_sink": false,
      "materially_distinct": true,
      "rationale": "Ruled out: source-wide search shows verify_signature has exactly one caller (find_consumer), which contains the fix. No alternate entry point to the sink exists. openid-connect uses a different JWT library (lua-resty-openidc), a separate codebase, not the same sink.",
      "exists": false
    },
    {
      "id": "R4_parsing_bypass",
      "candidate_description": "Crafted JWT header (duplicate alg keys, non-string alg, alg=none, unicode) that passes the fix's equality check but dispatches HMAC",
      "reaches_same_sink": false,
      "materially_distinct": true,
      "rationale": "Ruled out: resty.jwt load_jwt/parse_jwt sets header.alg from cjson.decode(raw_header).alg with no normalization. The fix's check and alg_verify dispatch both read the same self.header.alg, so no value can both equal expected_alg and dispatch to HMAC. Duplicate keys -> cjson keeps last (still mismatches expected). Non-string/nil alg -> type-mismatched inequality -> rejected. alg=none -> not in alg_verify and ~= expected -> rejected.",
      "exists": false
    }
  ],
  "overall_equivalence": "The runtime-confirmed candidates (V2 ES256, V3 EdDSA) are root-cause-identical to the parent CVE (same sink, entry point, trust boundary, technique) and are alternate triggers, not distinct variants. The materially-distinct candidates (R3 alternate entrypoint, R4 parsing bypass) do not exist. No bypass on the fixed version.",
  "same_root_cause_confidence": "high",
  "same_surface_confidence": "high"
}
