CVE-2026-48523: PyJWT: Algorithm allow-list bypass when decoding with `PyJWK` / `PyJWKClient` keys

Published May 28, 2026
·
Updated

> [!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.decodecomplete() 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.getsigningkeyfromjwt(...) 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/apijws.py in 2.12.1, verifysignature() treats PyJWK keys differently from normal PEM/public-key inputs:

python if algorithms is None and isinstance(key, PyJWK): algorithms = [key.algorithmname]

...

if not alg or (algorithms is not None and alg not in algorithms): raise InvalidAlgorithmError("The specified alg value is not allowed")

if isinstance(key, PyJWK): algobj = key.Algorithm preparedkey = key.key else: algobj = self.getalgorithmbyname(alg) preparedkey = algobj.preparekey(key)

This logic means:

1. The JWT header alg is checked only as a string against the caller-supplied allow-list. 2. If the key is a PyJWK, the actual verifier is not selected from the header algorithm. 3. Instead, PyJWT always verifies with key.Algorithm, which is fixed when the PyJWK object is created.

PyJWK binds its algorithm in jwt/apijwk.py from the JWK's alg field or from key-type defaults:

python if not algorithm and isinstance(self.jwkdata, dict): algorithm = self.jwkdata.get("alg", None)

...

self.algorithmname = algorithm self.Algorithm = getdefaultalgorithms()[algorithm] self.key = self.Algorithm.fromjwk(self.jwkdata)

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:

python signingkey = jwksclient.getsigningkeyfromjwt(token) jwt.decode( token, signingkey, audience="https://expenses-api", options={"verifyexp": False}, algorithms=["RS256"], )

PyJWKClient.getsigningkeyfromjwt() 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:

bash python -m pip install pyjwt==2.12.1 cryptography

Run the following script:

python import json import jwt from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from jwt.apijwk import PyJWK from jwt.algorithms import RSAAlgorithm from jwt.utils import base64urlencode

Generate an RSA keypair controlled by the attacker. priv = rsa.generateprivatekey(publicexponent=65537, keysize=2048) pub = priv.publickey() pubpem = pub.publicbytes(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.fromjson(RSAAlgorithm.tojwk(pub))

Create a token whose protected header claims RS512. header = {"typ": "JWT", "alg": "RS512"} payload = {"sub": "alice"}

headerb64 = base64urlencode( json.dumps(header, separators=(",", ":"), sortkeys=True).encode() ) payloadb64 = base64urlencode( json.dumps(payload, separators=(",", ":")).encode() ) signinginput = b".".join([headerb64, payloadb64])

Sign the RS512-labelled token with RS256 instead. sig = RSAAlgorithm(RSAAlgorithm.SHA256).sign(signinginput, priv) token = b".".join([headerb64, payloadb64, base64urlencode(sig)]).decode()

print("token:", token) print("PyJWK path:") print(jwt.decode(token, jwk, algorithms=["RS512"]))

print("PEM path:") try: print(jwt.decode(token, pubpem, algorithms=["RS512"])) except Exception as e: print(f"{type(e).name}: {e}")

Observed output:

text PyJWK path: {'sub': 'alice'} PEM path: InvalidSignatureError: Signature verification failed

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.decodecomplete() 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

Other sources

PyJWT is a JSON Web Token implementation in Python. From 2.9.0 to 2.12.1, there is a verifier-side algorithm allow-list bypass when jwt.decode() or jwt.decodecomplete() 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.getsigningkeyfromjwt(...) flow. This vulnerability is fixed in 2.13.0.

MITRE

Affected Software

3 affected componentsFixes available
pypi/pyjwt>=2.9.0<=2.12.1
Pyjwt Project Pyjwt>=2.9.0<2.12.1
pip/pyjwt>=2.9.0<2.13.0
2.13.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/pyjwt to a version that resolves this vulnerability.

    Fixed in 2.13.0
  2. Configuration

    Do not pass PyJWK objects returned by PyJWKClient.get_signing_key_from_jwt() directly to jwt.decode()/jwt.decode_complete(); instead obtain/verify public keys via the PEM/public-key path so verification selects the algorithm from the JWT header.

    PyJWT use_PyJWKClient.get_signing_key_from_jwt / PyJWK verification path = avoid
  3. Compensating control

    Limit who can register JWK/JWKS material (e.g., disable self-service JWK registration, require administrative approval) and restrict trust of external JWKS endpoints so untrusted parties cannot supply signing keys.

  4. Operational

    Audit and, if necessary, rotate or revoke any registered JWK/JWKS keys that may be controlled by untrusted parties or otherwise exposed, before or after applying the fix.

Event History

May 28, 2026
CVE Published
via MITRE·03:10 PM
Data Sourced
via MITRE·03:10 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·04:16 PM
DescriptionSeverityWeaknessAffected Software
Jun 15, 2026
Advisory Published
via GitHub·07:27 PM
Data Sourced
via GitHub·07:27 PM
DescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-48523?

The severity of CVE-2026-48523 is medium, with a score of 5.4.

2

What vulnerabilities does CVE-2026-48523 address?

CVE-2026-48523 addresses an algorithm allow-list bypass in the PyJWT library when decoding with PyJWK or PyJWKClient keys.

3

How do I fix CVE-2026-48523?

To fix CVE-2026-48523, update PyJWT to version 2.12.2 or later to ensure proper algorithm verification.

4

Which versions of PyJWT are affected by CVE-2026-48523?

CVE-2026-48523 affects PyJWT versions from 2.9.0 to 2.12.1.

5

What is the impact of CVE-2026-48523?

The impact of CVE-2026-48523 is that it allows attackers to potentially bypass the algorithm allow-list and accept malicious tokens.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203