GHSA-jq35-7prp-9v3f: PyJWT: Algorithm allow-list bypass when decoding with `PyJWK` / `PyJWKClient` keys
[!NOTE]
Scored assuming a deployment where algorithm policy functions as an authentication/authorization boundary. In deployments where the algorithm policy enforces crypto agility only, the practical confidentiality impact is lower and the issue is closer to an integrity-of-policy-enforcement bug.PyJWT `2.9.0` through `2.12.1` allows a verifier-side algorithm allow-list bypass when `jwt.decode()` or `jwt.decode_complete()` are called with a `PyJWK` key. The token header `alg` is checked against the caller-supplied `algorithms` allow-list, but signature verification is performed with the algorithm bound to the `PyJWK` object instead of the header algorithm. An attacker who controls a registered JWK/JWKS private key can sign with a disallowed algorithm, advertise an allowed algorithm in the JWT header, and still be accepted. The issue affects the documented `PyJWKClient.get_signing_key_from_jwt(...)` flow.SummaryPyJWT's `PyJWK` verification path allows a verifier-side algorithm allow-list bypass.In affected versions, when a JWT is decoded with a `PyJWK` object, PyJWT verifies that the header `alg` string is present in the caller's `algorithms=[...]` list, but it does not actually use the header algorithm to verify the signature. Instead, it verifies with the algorithm already bound to the `PyJWK` object.This lets an attacker who controls a registered JWK/JWKS private key sign with a disallowed algorithm and have the token accepted as long as the JWT header advertises an allowed algorithm. This affects the documented `PyJWKClient` usage flow and does not require any non-default flags or unsafe configuration.DetailsIn `jwt/api_jws.py` in `2.12.1`, `_verify_signature()` treats `PyJWK` keys differently from normal PEM/public-key inputs:This logic means:The JWT header `alg` is checked only as a string against the caller-supplied allow-list.If the key is a `PyJWK`, the actual verifier is not selected from the header algorithm.Instead, PyJWT always verifies with `key.Algorithm`, which is fixed when the `PyJWK` object is created.`PyJWK` binds its algorithm in `jwt/api_jwk.py` from the JWK's `alg` field or from key-type defaults:So once a `PyJWK` is constructed, the verifier uses the `PyJWK`'s bound algorithm, not the JWT header algorithm.The issue is reachable through the documented JWKS flow. In `docs/usage.rst`, the project documents:`PyJWKClient.get_signing_key_from_jwt()` returns a `PyJWK`, so this documented path is affected.This is not a "no-key forgery" issue. The attacker still needs control of an accepted JWK/JWKS private key. However, that is realistic in deployments such as:self-service OAuth client assertionsmulti-tenant key registrationfederation / BYO-JWKS trust modelsany system where external parties sign JWTs with their own registered keysIn those cases, the attacker can bypass verifier-side algorithm policy. For example, if the server intends to only accept `PS256`, an attacker controlling an accepted RSA JWK can sign with `RS256`, set `alg=PS256` in the JWT header, and still be accepted through the `PyJWK` path.The same forged token is rejected through the normal PEM/public-key verification path, which shows the bug is specific to `PyJWK` verification rather than expected JWT behavior.This behavior was introduced by commit `ab8176abe21e550dbc1c9a6bb7e78ad80853bfb1` (`Decode with PyJWK (#886)`), which is present in tagged releases `2.9.0`, `2.10.0`, `2.10.1`, `2.11.0`, `2.12.0`, and `2.12.1`.PoCTested locally against PyJWT `2.12.1` on Python `3.12.10` with `cryptography 45.0.6`.Install dependencies:Run the following script:Observed output:The token is accepted when the verification key is a `PyJWK`, even though:the caller restricted allowed algorithms to `["RS512"]`the signature was actually generated with `RS256`The same token is rejected when verified through the normal PEM/public-key path.ImpactThis is an algorithm allow-list bypass affecting `jwt.decode()` and `jwt.decode_complete()` when the verification key is a `PyJWK`, including keys returned by `PyJWKClient`.The impact depends on the deployment model:If attackers cannot control any accepted JWK/JWKS private key, practical exploitability is limited.If attackers can legitimately control a registered key, this is exploitable.Impacted deployments include:JWT client assertion flows where each client uses its own keymultitenant systems where tenants register JWK/JWKS materialfederation-style trust modelsany application that relies on `algorithms=[...]` to enforce a crypto policy against externally controlled signing keysWhat an attacker can do:bypass a server-side requirement such as "only `PS256`" or "only `RS512`"continue using a deprecated or blocked algorithm after the server thought it had disabled itauthenticate successfully as their own client / tenant / federation principal even though they do not satisfy the configured algorithm policyWhat this issue does not do by itself:it does not let an attacker forge tokens without access to a valid signing key or signing oracleit does not automatically enable cross-tenant impersonation unless the surrounding application trust model adds another flaw
CVSS v3.1: 5.4