A AegiFlow
HIGHCVSS 7.4

GHSA-pxh4-856f-4h89

PyJWT: HS256/384/512 verify accepts empty HMAC key via oct JWK (PyJWK/PyJWKClient) enabling token forgery

Published
2026-10-22
Modified
2026-10-22
Sources
github-advisory

Summary

### Summary `HMACAlgorithm.from_jwk` accepts a symmetric (`kty=oct`) JWK whose `k` value is the empty string and returns a zero-length key without raising. When a token is then verified with that key object (a `PyJWK`, or a `PyJWK` resolved by `PyJWKSet` / `PyJWKClient`), the HMAC verifier computes `HMAC(key=b"", signing_input)` and accepts any token an attacker forged with the same empty key. The attacker needs no secret knowledge: they reproduce the digest locally with an empty key and set the token's `kid` to the empty-`k` JWK. The empty-key rejection that `HMACAlgorithm.prepare_key` performs for string/bytes secrets (added in 2.13.0) was never propagated to the sibling `from_jwk` code path, so the JWK/JWKS verification route stays exploitable. Under default decode options this path only emits an `InsecureKeyLengthWarning`, not an error, so verification still succeeds. This is the same "empty/nil HMAC key is accepted by verify" defect the JWT ecosystem is currently closing library-side: fast-jwt CVE-2026-44351 (CVSS 9.1) and ruby-jwt CVE-2026-45363 both added a library-side reject-empty-HMAC-key precondition. ### Affected versions `PyJWT` (PyPI package `pyjwt`) ` bytes: try: if isinstance(jwk, str): obj: JWKDict = json.loads(jwk) elif isinstance(jwk, dict): obj = jwk else: raise ValueError except ValueError: raise InvalidKeyError("Key is not valid JSON") from None if obj.get("kty") != "oct": raise InvalidKeyError("Not an HMAC key") return base64url_decode(obj["k"]) # b"", no rejection ``` Contrast the sibling `prepare_key`, which DID get the guard (lines 325-329): ```python def prepare_key(self, key: str | bytes) -> bytes: key_bytes = force_bytes(key) if len(key_bytes) == 0: raise InvalidKeyError("HMAC key must not be empty.") ``` In `jwt/api_jws.py`, `_verify_signature` (lines 389-416) uses the `PyJWK` key material directly, bypassing `prepare_key`, and the length warning is non-fatal by default: ```python if isinstance(key, PyJWK): if alg != key.algorithm_name: raise InvalidAlgorithmError( f"Token algorithm {alg!r} does not match the key's " f"algorithm {key.algorithm_name!r}" ) alg_obj = key.Algorithm prepared_key = key.key # from_jwk output (b""), prepare_key not run else: try: alg_obj = self.get_algorithm_by_name(alg) except NotImplementedError as e: raise InvalidAlgorithmError("Algorithm not supported") from e prepared_key = alg_obj.prepare_key(key) key_length_msg = alg_obj.check_key_length(prepared_key) if key_length_msg: if effective_options.get("enforce_minimum_key_length", False):

Affected packages

EcosystemPackageAffected versionsFixed versions
PyPIpyjwt—2.14.0

Remediation: Upgrade to 2.14.0 or later.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.