You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
PyJWT: Algorithm allow-list bypass when decoding with `PyJWK` / `PyJWKClient` keys
Moderate severity
GitHub Reviewed
Published
May 21, 2026
in
jpadilla/pyjwt
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.
Summary
PyJWT'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.
Details
In jwt/api_jws.py in 2.12.1, _verify_signature() treats PyJWK keys differently from normal PEM/public-key inputs:
ifalgorithmsisNoneandisinstance(key, PyJWK):
algorithms= [key.algorithm_name]
...
ifnotalgor (algorithmsisnotNoneandalgnotinalgorithms):
raiseInvalidAlgorithmError("The specified alg value is not allowed")
ifisinstance(key, PyJWK):
alg_obj=key.Algorithmprepared_key=key.keyelse:
alg_obj=self.get_algorithm_by_name(alg)
prepared_key=alg_obj.prepare_key(key)
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:
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 assertions
multi-tenant key registration
federation / BYO-JWKS trust models
any system where external parties sign JWTs with their own registered keys
In 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.
PoC
Tested locally against PyJWT 2.12.1 on Python 3.12.10 with cryptography 45.0.6.
Install dependencies:
python -m pip install pyjwt==2.12.1 cryptography
Run the following script:
importjsonimportjwtfromcryptography.hazmat.primitives.asymmetricimportrsafromcryptography.hazmat.primitives.serializationimportEncoding, PublicFormatfromjwt.api_jwkimportPyJWKfromjwt.algorithmsimportRSAAlgorithmfromjwt.utilsimportbase64url_encode# Generate an RSA keypair controlled by the attacker.priv=rsa.generate_private_key(public_exponent=65537, key_size=2048)
pub=priv.public_key()
pub_pem=pub.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
# Build a PyJWK from the public key.# With an RSA JWK and no explicit alg, PyJWK binds to RS256 by default.jwk=PyJWK.from_json(RSAAlgorithm.to_jwk(pub))
# Create a token whose protected header claims RS512.header= {"typ": "JWT", "alg": "RS512"}
payload= {"sub": "alice"}
header_b64=base64url_encode(
json.dumps(header, separators=(",", ":"), sort_keys=True).encode()
)
payload_b64=base64url_encode(
json.dumps(payload, separators=(",", ":")).encode()
)
signing_input=b".".join([header_b64, payload_b64])
# Sign the RS512-labelled token with RS256 instead.sig=RSAAlgorithm(RSAAlgorithm.SHA256).sign(signing_input, priv)
token=b".".join([header_b64, payload_b64, base64url_encode(sig)]).decode()
print("token:", token)
print("PyJWK path:")
print(jwt.decode(token, jwk, algorithms=["RS512"]))
print("PEM path:")
try:
print(jwt.decode(token, pub_pem, algorithms=["RS512"]))
exceptExceptionase:
print(f"{type(e).__name__}: {e}")
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.
Impact
This 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 key
multitenant systems where tenants register JWK/JWKS material
federation-style trust models
any application that relies on algorithms=[...] to enforce a crypto policy against externally controlled signing keys
What 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 it
authenticate successfully as their own client / tenant / federation principal even though they do not satisfy the configured algorithm policy
What this issue does not do by itself:
it does not let an attacker forge tokens without access to a valid signing key or signing oracle
it does not automatically enable cross-tenant impersonation unless the surrounding application trust model adds another flaw
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.0through2.12.1allows a verifier-side algorithm allow-list bypass whenjwt.decode()orjwt.decode_complete()are called with aPyJWKkey. The token headeralgis checked against the caller-suppliedalgorithmsallow-list, but signature verification is performed with the algorithm bound to thePyJWKobject 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 documentedPyJWKClient.get_signing_key_from_jwt(...)flow.Summary
PyJWT's
PyJWKverification path allows a verifier-side algorithm allow-list bypass.In affected versions, when a JWT is decoded with a
PyJWKobject, PyJWT verifies that the headeralgstring is present in the caller'salgorithms=[...]list, but it does not actually use the header algorithm to verify the signature. Instead, it verifies with the algorithm already bound to thePyJWKobject.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
PyJWKClientusage flow and does not require any non-default flags or unsafe configuration.Details
In
jwt/api_jws.pyin2.12.1,_verify_signature()treatsPyJWKkeys differently from normal PEM/public-key inputs:This logic means:
algis checked only as a string against the caller-supplied allow-list.PyJWK, the actual verifier is not selected from the header algorithm.key.Algorithm, which is fixed when thePyJWKobject is created.PyJWKbinds its algorithm injwt/api_jwk.pyfrom the JWK'salgfield or from key-type defaults:So once a
PyJWKis constructed, the verifier uses thePyJWK'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 aPyJWK, 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:
In 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 withRS256, setalg=PS256in the JWT header, and still be accepted through thePyJWKpath.The same forged token is rejected through the normal PEM/public-key verification path, which shows the bug is specific to
PyJWKverification rather than expected JWT behavior.This behavior was introduced by commit
ab8176abe21e550dbc1c9a6bb7e78ad80853bfb1(Decode with PyJWK (#886)), which is present in tagged releases2.9.0,2.10.0,2.10.1,2.11.0,2.12.0, and2.12.1.PoC
Tested locally against PyJWT
2.12.1on Python3.12.10withcryptography 45.0.6.Install dependencies:
Run the following script:
Observed output:
The token is accepted when the verification key is a
PyJWK, even though:["RS512"]RS256The same token is rejected when verified through the normal PEM/public-key path.
Impact
This is an algorithm allow-list bypass affecting
jwt.decode()andjwt.decode_complete()when the verification key is aPyJWK, including keys returned byPyJWKClient.The impact depends on the deployment model:
Impacted deployments include:
algorithms=[...]to enforce a crypto policy against externally controlled signing keysWhat an attacker can do:
PS256" or "onlyRS512"What this issue does not do by itself:
References