VYPR
Critical severityNVD Advisory· Published May 15, 2026· Updated May 18, 2026

CVE-2026-44699

CVE-2026-44699

Description

LibJWT is a C JSON Web Token Library. From 3.0.0 to 3.3.2, libjwt accepts an RSA JWK that does not contain an alg parameter as the verification key for an HS256/HS384/HS512 token. In the OpenSSL backend, this causes HMAC verification to run with a zero-length key, so an attacker can forge a valid JWT without knowing any secret or RSA private key. This is an algorithm-confusion authentication bypass. It affects applications that load RSA keys from JWKS where alg is omitted, which is valid JWK syntax and common in real deployments, and then choose the verification algorithm from the JWT header, for example in a kid lookup callback. This vulnerability is fixed in 3.3.3.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

LibJWT 3.0.0–3.3.2 accepts an RSA JWK without an alg parameter as an HMAC key, allowing JWT forgery without secrets.

Vulnerability

LibJWT versions 3.0.0 through 3.3.2 contain an algorithm-confusion authentication bypass in the OpenSSL backend. When an application loads an RSA JWK that does not specify an alg parameter (valid JWK syntax; key->alg == JWT_ALG_NONE), the __setkey_check() function in jwt-common.c accepts any non-none algorithm. The parsed JWK stores the RSA key in a union that overlaps with the octet key field for HMAC. Subsequently, HMAC verification in jwt-verify.c uses jwt->key->oct.key and jwt->key->oct.len — the latter is zero for the RSA key. OpenSSL accepts a zero-length key, so the token is verified as if signed with an empty HMAC secret. This affects applications that fetch RSA keys from a JWKS endpoint where alg is omitted and then select the verification algorithm from the JWT header (e.g., via a kid lookup callback). [1]

Exploitation

An attacker does not need any prior authentication, secret, or RSA private key. The attacker constructs a JWT with an alg header set to HS256, HS384, or HS512, a payload of their choosing, and computes an HMAC signature using an empty key (zero-length). The attacker sends this forged token to a server that uses LibJWT and loads an RSA JWK without an alg parameter. The server's verification code accepts the token because the zero-length key is accepted by OpenSSL and the key's missing alg bypasses the algorithm check. [1]

Impact

Successful exploitation allows an attacker to forge arbitrary valid JWTs that will be accepted as authentic by the affected application. This leads to authentication bypass, enabling the attacker to impersonate any user, escalate privileges, or access any resource protected by the compromised token validation. The impact is a complete compromise of authentication, with high confidentiality, integrity, and availability impact depending on the application's authorization boundaries. [1]

Mitigation

A fix is available in LibJWT version 3.3.3, released on 2026-05-15. Users must upgrade to 3.3.3 or later. No workarounds are documented; until patched, ensure all JWKs in use include the alg field and that the application enforces algorithm matching against the key type, not just the alg hint. This vulnerability is not listed on CISA's KEV as of the publication date. [1]

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
49c730a4036c

Fix algorithm confusion that allows JWT forgery via RSA JWK as HMAC key

https://github.com/benmcollins/libjwtBen CollinsMay 4, 2026Fixed in 3.3.3via llm-release-walk
10 files changed · +377 57
  • libjwt/jwt.c+13 0 modified
    @@ -140,6 +140,19 @@ static int __check_hmac(jwt_t *jwt)
     {
     	int key_bits = jwt->key->bits;
     
    +	/* Defensive: an HMAC algorithm requires an octet key. Without this
    +	 * check, an RSA/EC/OKP key reaches the backend's HMAC routines, which
    +	 * read jwt->key->oct.{key,len} from a union shared with provider_data
    +	 * and would HMAC against a zero-length key (GHSA-q843-6q5f-w55g).
    +	 * The earlier __setkey_check / __verify_config_post bindings make
    +	 * this unreachable in practice, but keep the check as a backstop. */
    +	// LCOV_EXCL_START
    +	if (jwt->key->kty != JWK_KEY_TYPE_OCT) {
    +		jwt_write_error(jwt, "Key type does not match HMAC alg");
    +		return 1;
    +	}
    +	// LCOV_EXCL_STOP
    +
     	switch (jwt->alg) {
     	case JWT_ALG_HS256:
     		if (key_bits >= 256)
    
  • libjwt/jwt-common.c+14 1 modified
    @@ -82,7 +82,20 @@ static int __setkey_check(jwt_common_t *__cmd, const jwt_alg_t alg,
     			return 0;
     
     		jwt_write_error(__cmd, "Cannot set alg without a key");
    -	} else if (key->alg == JWT_ALG_NONE) {
    +		return 1;
    +	}
    +
    +	/* Bind algorithm to the JWK's actual key type, not just the
    +	 * optional "alg" hint. The "alg" parameter on a JWK is optional
    +	 * (RFC 7517 4.4), so we must never let its absence widen what a
    +	 * key can be used for. */
    +	if (alg != JWT_ALG_NONE && jwt_alg_required_kty(alg) != key->kty) {
    +		jwt_write_error(__cmd,
    +			"Key type does not match algorithm");
    +		return 1;
    +	}
    +
    +	if (key->alg == JWT_ALG_NONE) {
     		if (alg != JWT_ALG_NONE)
     			return 0;
     
    
  • libjwt/jwt-private.h+37 0 modified
    @@ -253,4 +253,41 @@ int jwt_head_setup(jwt_t *jwt);
     
     #define __trace() fprintf(stderr, "%s:%d\n", __func__, __LINE__)
     
    +/* Returns the jwk_key_type_t that the given JWA algorithm requires, or
    + * JWK_KEY_TYPE_NONE for JWT_ALG_NONE / unknown values. This is the
    + * authoritative kty<->alg mapping used to prevent algorithm confusion
    + * attacks (e.g. GHSA-q843-6q5f-w55g): an RSA JWK must never be usable
    + * for an HS* token, regardless of whether the JWK carries an alg hint. */
    +static inline jwk_key_type_t jwt_alg_required_kty(jwt_alg_t alg)
    +{
    +	switch (alg) {
    +	case JWT_ALG_HS256:
    +	case JWT_ALG_HS384:
    +	case JWT_ALG_HS512:
    +		return JWK_KEY_TYPE_OCT;
    +
    +	case JWT_ALG_RS256:
    +	case JWT_ALG_RS384:
    +	case JWT_ALG_RS512:
    +	case JWT_ALG_PS256:
    +	case JWT_ALG_PS384:
    +	case JWT_ALG_PS512:
    +		return JWK_KEY_TYPE_RSA;
    +
    +	case JWT_ALG_ES256:
    +	case JWT_ALG_ES256K:
    +	case JWT_ALG_ES384:
    +	case JWT_ALG_ES512:
    +		return JWK_KEY_TYPE_EC;
    +
    +	case JWT_ALG_EDDSA:
    +		return JWK_KEY_TYPE_OKP;
    +
    +	// LCOV_EXCL_START
    +	default:
    +		return JWK_KEY_TYPE_NONE;
    +	// LCOV_EXCL_STOP
    +	}
    +}
    +
     #endif /* JWT_PRIVATE_H */
    
  • libjwt/jwt-verify.c+9 0 modified
    @@ -260,6 +260,15 @@ static int __verify_config_post(jwt_t *jwt, const jwt_config_t *config,
     		// LCOV_EXCL_STOP
     	}
     
    +	/* Algorithm is now bound (jwt->alg). Defensively confirm that the
    +	 * JWK's actual key type can carry it. This blocks algorithm
    +	 * confusion (GHSA-q843-6q5f-w55g) even if a malformed JWK has an
    +	 * "alg" hint that disagrees with its "kty". */
    +	if (jwt_alg_required_kty(jwt->alg) != config->key->kty) {
    +		jwt_write_error(jwt, "Key type does not match JWT alg");
    +		return 1;
    +	}
    +
     	return 0;
     }
     
    
  • tests/jwt_builder.c+6 13 modified
    @@ -825,8 +825,6 @@ END_TEST
     START_TEST(sign_es256_bad_sig)
     {
     	jwt_builder_auto_t *builder = NULL;
    -	const char *err;
    -	char *out;
     	int ret;
     
     	SET_OPS();
    @@ -835,19 +833,14 @@ START_TEST(sign_es256_bad_sig)
     	ck_assert_ptr_nonnull(builder);
     	ck_assert_int_eq(jwt_builder_error(builder), 0);
     
    +	/* Algorithm confusion: an OKP/EdDSA JWK must not be settable for
    +	 * an EC algorithm like ES256 (GHSA-q843-6q5f-w55g). setkey rejects
    +	 * up front before any signing is attempted. */
     	read_json("eddsa_key_ed25519_fake_es256.json");
    -
     	ret = jwt_builder_setkey(builder, JWT_ALG_ES256, g_item);
    -	fprintf(stderr, "%s\n", jwt_builder_error_msg(builder));
    -	ck_assert_int_eq(ret, 0);
    -
    -	out = jwt_builder_generate(builder);
    -	ck_assert_ptr_null(out);
    -
    -	err = jwt_builder_error_msg(builder);
    -	ck_assert_ptr_nonnull(err);
    -	/* Fails in different ways depending on the backend */
    -	ck_assert_mem_eq(err, "JWT[", 4);
    +	ck_assert_int_ne(ret, 0);
    +	ck_assert_str_eq(jwt_builder_error_msg(builder),
    +			"Key type does not match algorithm");
     
     	free_key();
     }
    
  • tests/jwt_checker.c+8 20 modified
    @@ -435,8 +435,6 @@ END_TEST
     START_TEST(hs256_token_failed)
     {
     	jwt_checker_auto_t *checker = NULL;
    -	const char token[] = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.CM4dD95Nj"
    -		"0vSfMGtDas432AUW1HAo7feCiAbt5Yjuds";
     	int ret;
     
     	SET_OPS();
    @@ -445,14 +443,13 @@ START_TEST(hs256_token_failed)
     	ck_assert_ptr_nonnull(checker);
     	ck_assert_int_eq(jwt_checker_error(checker), 0);
     
    +	/* Algorithm confusion: an OKP/EdDSA JWK must not be settable for
    +	 * HS256 (GHSA-q843-6q5f-w55g). setkey rejects up front. */
     	read_json("eddsa_key_ed25519_pub.json");
     	ret = jwt_checker_setkey(checker, JWT_ALG_HS256, g_item);
    -	ck_assert_int_eq(ret, 0);
    -
    -	ret = jwt_checker_verify(checker, token);
     	ck_assert_int_ne(ret, 0);
     	ck_assert_str_eq(jwt_checker_error_msg(checker),
    -			"Token failed verification");
    +			"Key type does not match algorithm");
     
     	free_key();
     }
    @@ -881,11 +878,6 @@ END_TEST
     START_TEST(verify_es256_bad_sig)
     {
     	jwt_checker_auto_t *checker = NULL;
    -	const char token[] = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI"
    -		"xMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlh"
    -		"dCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqU"
    -		"SLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA";
    -	const char *err;
     	int ret;
     
     	SET_OPS();
    @@ -894,18 +886,14 @@ START_TEST(verify_es256_bad_sig)
     	ck_assert_ptr_nonnull(checker);
     	ck_assert_int_eq(jwt_checker_error(checker), 0);
     
    +	/* Algorithm confusion: an OKP/EdDSA JWK must not be settable for
    +	 * an EC algorithm like ES256 (GHSA-q843-6q5f-w55g). setkey rejects
    +	 * up front before the broken-signature path is ever reached. */
     	read_json("eddsa_key_ed25519_pub_fake_es256.json");
    -
     	ret = jwt_checker_setkey(checker, JWT_ALG_ES256, g_item);
    -	ck_assert_int_eq(ret, 0);
    -
    -	ret = jwt_checker_verify(checker, token);
     	ck_assert_int_ne(ret, 0);
    -
    -	err = jwt_checker_error_msg(checker);
    -	ck_assert_ptr_nonnull(err);
    -	/* Fails in different ways depending on the backend */
    -	ck_assert_mem_eq(err, "JWT[", 4);
    +	ck_assert_str_eq(jwt_checker_error_msg(checker),
    +			"Key type does not match algorithm");
     
     	free_key();
     }
    
  • tests/jwt_rsa.c+17 23 modified
    @@ -208,48 +208,42 @@ END_TEST
     
     START_TEST(rsa_ec_short)
     {
    -        jwt_builder_auto_t *builder = NULL;
    -        char *out = NULL;
    -        int ret;
    +	jwt_builder_auto_t *builder = NULL;
    +	int ret;
     
     	SET_OPS();
     
     	builder = jwt_builder_new();
     	ck_assert_ptr_nonnull(builder);
     	ck_assert_int_eq(jwt_builder_error(builder), 0);
     
    +	/* Algorithm confusion: an RSA JWK must not be settable for EC or
    +	 * EdDSA algorithms (GHSA-q843-6q5f-w55g). setkey rejects up front
    +	 * regardless of which incompatible alg is requested. */
     	read_json("rsa_key_1024.json");
    -	ret = jwt_builder_setkey(builder, JWT_ALG_ES256, g_item);
    -        ck_assert_int_eq(ret, 0);
     
    -	out = jwt_builder_generate(builder);
    -	ck_assert_ptr_null(out);
    +	ret = jwt_builder_setkey(builder, JWT_ALG_ES256, g_item);
    +	ck_assert_int_ne(ret, 0);
     	ck_assert_str_eq(jwt_builder_error_msg(builder),
    -			"Key needs to be 256 bits: 1024 bits");
    +			"Key type does not match algorithm");
    +	jwt_builder_error_clear(builder);
     
     	ret = jwt_builder_setkey(builder, JWT_ALG_EDDSA, g_item);
    -	ck_assert_int_eq(ret, 0);
    -
    -	out = jwt_builder_generate(builder);
    -	ck_assert_ptr_null(out);
    +	ck_assert_int_ne(ret, 0);
     	ck_assert_str_eq(jwt_builder_error_msg(builder),
    -			"Key needs to be 256 or 456 bits: 1024 bits");
    +			"Key type does not match algorithm");
    +	jwt_builder_error_clear(builder);
     
     	ret = jwt_builder_setkey(builder, JWT_ALG_ES384, g_item);
    -	ck_assert_int_eq(ret, 0);
    -
    -	out = jwt_builder_generate(builder);
    -	ck_assert_ptr_null(out);
    +	ck_assert_int_ne(ret, 0);
     	ck_assert_str_eq(jwt_builder_error_msg(builder),
    -			"Key needs to be 384 bits: 1024 bits");
    +			"Key type does not match algorithm");
    +	jwt_builder_error_clear(builder);
     
     	ret = jwt_builder_setkey(builder, JWT_ALG_ES512, g_item);
    -	ck_assert_int_eq(ret, 0);
    -
    -	out = jwt_builder_generate(builder);
    -	ck_assert_ptr_null(out);
    +	ck_assert_int_ne(ret, 0);
     	ck_assert_str_eq(jwt_builder_error_msg(builder),
    -			"Key needs to be 521 bits: 1024 bits");
    +			"Key type does not match algorithm");
     
     	free_key();
     }
    
  • tests/jwt_security.c+248 0 modified
    @@ -833,6 +833,230 @@ START_TEST(test_jwks_rsa_pss_alg_string)
     }
     END_TEST
     
    +/*
    + * === Algorithm Confusion Regression (GHSA-q843-6q5f-w55g) ===
    + *
    + * An RSA JWK with no "alg" hint must not be usable for any HMAC
    + * algorithm. Without the fix, libjwt would accept the RSA JWK for
    + * HS256/384/512 and run HMAC against a zero-length key
    + * (oct.key/oct.len read from the union shared with provider_data).
    + * That allowed an attacker who only knows the public JWKS to forge
    + * tokens that verify successfully.
    + */
    +
    +/* The exact PoC from the advisory: RSA public JWK without "alg",
    + * verifying an HS256 token whose signature is HMAC-SHA256("", header.payload).
    + * Must be rejected before the HMAC ever runs. */
    +START_TEST(test_alg_confusion_rsa_no_alg_hs256)
    +{
    +	jwt_checker_auto_t *checker = NULL;
    +	int ret;
    +
    +	SET_OPS();
    +
    +	checker = jwt_checker_new();
    +	ck_assert_ptr_nonnull(checker);
    +
    +	read_json("rsa_key_2048_pub_no_alg.json");
    +
    +	/* The setkey call itself must reject the kty/alg mismatch. */
    +	ret = jwt_checker_setkey(checker, JWT_ALG_HS256, g_item);
    +	ck_assert_int_ne(ret, 0);
    +	ck_assert_str_eq(jwt_checker_error_msg(checker),
    +			"Key type does not match algorithm");
    +
    +	free_key();
    +}
    +END_TEST
    +
    +/* Same defense for HS384 and HS512. */
    +START_TEST(test_alg_confusion_rsa_no_alg_hs384)
    +{
    +	jwt_checker_auto_t *checker = NULL;
    +	int ret;
    +
    +	SET_OPS();
    +
    +	checker = jwt_checker_new();
    +	ck_assert_ptr_nonnull(checker);
    +
    +	read_json("rsa_key_2048_pub_no_alg.json");
    +
    +	ret = jwt_checker_setkey(checker, JWT_ALG_HS384, g_item);
    +	ck_assert_int_ne(ret, 0);
    +	ck_assert_str_eq(jwt_checker_error_msg(checker),
    +			"Key type does not match algorithm");
    +
    +	free_key();
    +}
    +END_TEST
    +
    +START_TEST(test_alg_confusion_rsa_no_alg_hs512)
    +{
    +	jwt_checker_auto_t *checker = NULL;
    +	int ret;
    +
    +	SET_OPS();
    +
    +	checker = jwt_checker_new();
    +	ck_assert_ptr_nonnull(checker);
    +
    +	read_json("rsa_key_2048_pub_no_alg.json");
    +
    +	ret = jwt_checker_setkey(checker, JWT_ALG_HS512, g_item);
    +	ck_assert_int_ne(ret, 0);
    +	ck_assert_str_eq(jwt_checker_error_msg(checker),
    +			"Key type does not match algorithm");
    +
    +	free_key();
    +}
    +END_TEST
    +
    +/* RSA JWK that DOES carry an "alg" hint of RS256 must still be rejected
    + * for HS256: the prior code allowed it whenever the JWK alg matched the
    + * caller alg, but here the kty would still be RSA. We assert the broader
    + * invariant: HS* requires kty=oct period. */
    +START_TEST(test_alg_confusion_rsa_with_alg_hs256)
    +{
    +	jwt_checker_auto_t *checker = NULL;
    +	int ret;
    +
    +	SET_OPS();
    +
    +	checker = jwt_checker_new();
    +	ck_assert_ptr_nonnull(checker);
    +
    +	read_json("rsa_key_2048_pub.json");
    +
    +	ret = jwt_checker_setkey(checker, JWT_ALG_HS256, g_item);
    +	ck_assert_int_ne(ret, 0);
    +	ck_assert_str_eq(jwt_checker_error_msg(checker),
    +			"Key type does not match algorithm");
    +
    +	free_key();
    +}
    +END_TEST
    +
    +/* EC JWK must not be usable for an HMAC algorithm either. */
    +START_TEST(test_alg_confusion_ec_hs256)
    +{
    +	jwt_checker_auto_t *checker = NULL;
    +	int ret;
    +
    +	SET_OPS();
    +
    +	checker = jwt_checker_new();
    +	ck_assert_ptr_nonnull(checker);
    +
    +	read_json("ec_key_prime256v1_pub.json");
    +
    +	ret = jwt_checker_setkey(checker, JWT_ALG_HS256, g_item);
    +	ck_assert_int_ne(ret, 0);
    +	ck_assert_str_eq(jwt_checker_error_msg(checker),
    +			"Key type does not match algorithm");
    +
    +	free_key();
    +}
    +END_TEST
    +
    +/* OKP/EdDSA JWK must not be usable for an HMAC algorithm either. */
    +START_TEST(test_alg_confusion_okp_hs256)
    +{
    +	jwt_checker_auto_t *checker = NULL;
    +	int ret;
    +
    +	SET_OPS();
    +
    +	checker = jwt_checker_new();
    +	ck_assert_ptr_nonnull(checker);
    +
    +	read_json("eddsa_key_ed25519_pub.json");
    +
    +	ret = jwt_checker_setkey(checker, JWT_ALG_HS256, g_item);
    +	ck_assert_int_ne(ret, 0);
    +	ck_assert_str_eq(jwt_checker_error_msg(checker),
    +			"Key type does not match algorithm");
    +
    +	free_key();
    +}
    +END_TEST
    +
    +/* The realistic application pattern: a JWKS callback that picks a key
    + * by "kid" and copies the JWT header alg into config->alg. The attacker
    + * controls the header alg. With the fix, the verify path's __setkey_check
    + * (run after the callback) rejects the kty/alg mismatch. */
    +static int alg_confusion_cb(jwt_t *jwt, jwt_config_t *config)
    +{
    +	/* g_item is the RSA-no-alg JWK loaded by the test. */
    +	config->key = g_item;
    +	config->alg = jwt_get_alg(jwt);
    +	return 0;
    +}
    +
    +START_TEST(test_alg_confusion_callback_rsa_no_alg)
    +{
    +	jwt_checker_auto_t *checker = NULL;
    +	const char token[] =
    +		"eyJhbGciOiJIUzI1NiIsImtpZCI6InJzYS1uby1hbGcifQ"
    +		".eyJzdWIiOiJhZG1pbiJ9"
    +		".I2Ey63EMS9lOFEL93tQM8eB8cCnH6QJy0rIe1HVEI3I";
    +	int ret;
    +
    +	SET_OPS();
    +
    +	checker = jwt_checker_new();
    +	ck_assert_ptr_nonnull(checker);
    +
    +	read_json("rsa_key_2048_pub_no_alg.json");
    +
    +	ret = jwt_checker_setcb(checker, alg_confusion_cb, NULL);
    +	ck_assert_int_eq(ret, 0);
    +
    +	/* The forged token must NOT verify. */
    +	ret = jwt_checker_verify(checker, token);
    +	ck_assert_int_ne(ret, 0);
    +	ck_assert_str_eq(jwt_checker_error_msg(checker),
    +			"Key type does not match algorithm");
    +
    +	free_key();
    +}
    +END_TEST
    +
    +/* Defense in depth: a malformed JWK whose "alg" hint disagrees with its
    + * "kty" (here kty=RSA, alg=HS256) must not be usable for HMAC. The caller
    + * sets alg=NONE so __setkey_check defers to the JWK's alg hint, but the
    + * verify path then double-checks kty against the bound algorithm. */
    +START_TEST(test_alg_confusion_malformed_jwk_kty_alg)
    +{
    +	jwt_checker_auto_t *checker = NULL;
    +	const char token[] =
    +		"eyJhbGciOiJIUzI1NiIsImtpZCI6InJzYS1uby1hbGcifQ"
    +		".eyJzdWIiOiJhZG1pbiJ9"
    +		".I2Ey63EMS9lOFEL93tQM8eB8cCnH6QJy0rIe1HVEI3I";
    +	int ret;
    +
    +	SET_OPS();
    +
    +	checker = jwt_checker_new();
    +	ck_assert_ptr_nonnull(checker);
    +
    +	read_json("rsa_key_2048_pub_alg_hs256.json");
    +
    +	/* alg=NONE: caller trusts whatever the JWK says. The JWK lies
    +	 * (kty=RSA, alg=HS256). __setkey_check accepts because alg=NONE,
    +	 * but the verify path's defensive kty switch must reject. */
    +	ret = jwt_checker_setkey(checker, JWT_ALG_NONE, g_item);
    +	ck_assert_int_eq(ret, 0);
    +
    +	ret = jwt_checker_verify(checker, token);
    +	ck_assert_int_ne(ret, 0);
    +	ck_assert_str_eq(jwt_checker_error_msg(checker),
    +			"Key type does not match JWT alg");
    +
    +	free_key();
    +}
    +END_TEST
    +
     /*
      * === Suite Setup ===
      */
    @@ -843,6 +1067,7 @@ static Suite *libjwt_suite(const char *title)
     	TCase *tc_jwks_json;
     	TCase *tc_jwt_parse;
     	TCase *tc_null_safety;
    +	TCase *tc_alg_confusion;
     	int i = ARRAY_SIZE(jwt_test_ops);
     
     	s = suite_create(title);
    @@ -906,6 +1131,29 @@ static Suite *libjwt_suite(const char *title)
     	tcase_set_timeout(tc_null_safety, 30);
     	suite_add_tcase(s, tc_null_safety);
     
    +	/* Algorithm confusion regression (GHSA-q843-6q5f-w55g) */
    +	tc_alg_confusion = tcase_create("alg_confusion");
    +
    +	tcase_add_loop_test(tc_alg_confusion,
    +			    test_alg_confusion_rsa_no_alg_hs256, 0, i);
    +	tcase_add_loop_test(tc_alg_confusion,
    +			    test_alg_confusion_rsa_no_alg_hs384, 0, i);
    +	tcase_add_loop_test(tc_alg_confusion,
    +			    test_alg_confusion_rsa_no_alg_hs512, 0, i);
    +	tcase_add_loop_test(tc_alg_confusion,
    +			    test_alg_confusion_rsa_with_alg_hs256, 0, i);
    +	tcase_add_loop_test(tc_alg_confusion,
    +			    test_alg_confusion_ec_hs256, 0, i);
    +	tcase_add_loop_test(tc_alg_confusion,
    +			    test_alg_confusion_okp_hs256, 0, i);
    +	tcase_add_loop_test(tc_alg_confusion,
    +			    test_alg_confusion_callback_rsa_no_alg, 0, i);
    +	tcase_add_loop_test(tc_alg_confusion,
    +			    test_alg_confusion_malformed_jwk_kty_alg, 0, i);
    +
    +	tcase_set_timeout(tc_alg_confusion, 30);
    +	suite_add_tcase(s, tc_alg_confusion);
    +
     	return s;
     }
     
    
  • tests/keys/rsa_key_2048_pub_alg_hs256.json+13 0 added
    @@ -0,0 +1,13 @@
    +{
    +  "keys": [
    +    {
    +      "use": "sig",
    +      "key_ops": ["verify"],
    +      "alg": "HS256",
    +      "kid": "rsa-mislabeled-hs256",
    +      "kty": "RSA",
    +      "n": "wtpMAM4l1H995oqlqdMhuqNuffp4-4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry-iBs-z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NMZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD-b3Mcsjlh0yvSq7g6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS_85M4Y6Ss_T-OWi1OeK49NdYBvFP-hNVEoeZzJz5K_nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720tBw",
    +      "e": "AQAB"
    +    }
    +  ]
    +}
    
  • tests/keys/rsa_key_2048_pub_no_alg.json+12 0 added
    @@ -0,0 +1,12 @@
    +{
    +  "keys": [
    +    {
    +      "use": "sig",
    +      "key_ops": ["verify"],
    +      "kid": "rsa-no-alg",
    +      "kty": "RSA",
    +      "n": "wtpMAM4l1H995oqlqdMhuqNuffp4-4aUCwuFE9B5s9MJr63gyf8jW0oDr7Mb1Xb8y9iGkWfhouZqNJbMFry-iBs-z2TtJF06vbHQZzajDsdux3XVfXv9v6dDIImyU24MsGNkpNt0GISaaiqv51NMZQX0miOXXWdkQvWTZFXhmsFCmJLE67oQFSar4hzfAaCulaMD-b3Mcsjlh0yvSq7g6swiIasEU3qNLKaJAZEzfywroVYr3BwM1IiVbQeKgIkyPS_85M4Y6Ss_T-OWi1OeK49NdYBvFP-hNVEoeZzJz5K_nd6C35IX0t2bN5CVXchUFmaUMYk2iPdhXdsC720tBw",
    +      "e": "AQAB"
    +    }
    +  ]
    +}
    

Vulnerability mechanics

Root cause

"Missing key-type validation allows an RSA JWK (kty=RSA) without an alg parameter to be accepted as the verification key for HMAC algorithms, causing the OpenSSL backend to read a zero-length key from a union member and successfully verify any token signed with HMAC("", header.payload)."

Attack vector

An attacker who knows the victim's public RSA JWKS can forge a valid JWT by setting the JWT header algorithm to HS256/HS384/HS512 and signing the token with HMAC-SHA256("", header.payload). The attacker must ensure the JWK used for verification lacks an "alg" parameter (valid per RFC 7517 §4.4) and that the application selects the verification algorithm from the JWT header, for example via a kid lookup callback. Because libjwt's internal union shares storage between the RSA EVP_PKEY pointer and the HMAC octet key, the HMAC path reads a zero-length key and accepts the forged signature. The vulnerability is triggered over the network when the application verifies an incoming JWT against a JWKS endpoint [CWE-327][CWE-347].

Affected code

The vulnerability exists in the OpenSSL backend's HMAC path in `libjwt/jwt.c`, where `__check_hmac()` reads `jwt->key->oct.{key,len}` from a union that shares storage with `provider_data` (RSA EVP_PKEY *). The root cause is the absence of key-type validation in `libjwt/jwt-common.c`'s `__setkey_check()` and `libjwt/jwt-verify.c`'s `__verify_config_post()`, which previously only checked the JWK's optional "alg" hint rather than its mandatory "kty" field. The fix adds the authoritative mapping in `libjwt/jwt-private.h` via `jwt_alg_required_kty()`.

What the fix does

The patch adds three layered defenses. First, a new `jwt_alg_required_kty()` helper in `jwt-private.h` [patch_id=831183] maps each JWA algorithm to its required JWK key type (e.g., HS* → OCT, RS* → RSA). Second, `__setkey_check()` in `jwt-common.c` [patch_id=831183] rejects any `setkey()` call where the algorithm's required kty does not match the JWK's actual kty, regardless of whether the JWK carries an optional "alg" hint. Third, `__verify_config_post()` in `jwt-verify.c` [patch_id=831183] re-checks kty once the algorithm is bound from the token, and `__check_hmac()` in `jwt.c` [patch_id=831183] adds a defensive backstop refusing non-OCT keys for HMAC. Together these ensure an RSA/EC/OKP key can never be used for an HMAC algorithm, closing the union-based zero-length key read.

Preconditions

  • configApplication loads RSA keys from a JWKS endpoint where the JWK entries omit the optional 'alg' parameter
  • authApplication selects the verification algorithm from the JWT header (e.g., via a kid lookup callback) rather than fixing it per key
  • networkAttacker can submit a crafted JWT to the application's verification endpoint
  • inputAttacker controls the JWT header's 'alg' field and can forge an HMAC-SHA256 signature with an empty key

Generated on May 20, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.