Introduction
A partial security fix applied four years ago to PyJWT, the most popular JWT library in the Python ecosystem, left a gap that allows attackers to forge arbitrary authentication tokens by exploiting a different key format than the one originally blocked. CVE-2026-48526 is the result: a JWK format bypass of the algorithm confusion protections introduced in PyJWT 2.4.0, carrying a CVSS v3.1 score of 7.4 and enabling full authentication bypass when specific preconditions are met.
PyJWT implements RFC 7519 (JSON Web Token) for Python and is maintained by José Padilla. With over 5,700 GitHub stars and deep integration across Django, Flask, FastAPI, and Auth0's own Python SDK, it is the de facto standard for JWT handling in the Python ecosystem. Any vulnerability in PyJWT carries a substantial blast radius across Python web applications that rely on token based authentication.
Technical Information
Root Cause: JWK JSON Bypasses HMAC Key Validation
The vulnerability lives in the HMACAlgorithm.prepare_key method within jwt/algorithms.py. This method is the gatekeeper for key material used in HMAC signing and verification operations. When CVE-2022-29217 was fixed in PyJWT 2.4.0, the method was updated to detect and reject PEM formatted and SSH formatted public keys, raising an InvalidKeyError if either format was detected. The logic was straightforward: if someone passes an asymmetric public key where an HMAC secret is expected, something has gone wrong.
However, the fix used a blocklist approach, explicitly checking for known dangerous formats rather than whitelisting acceptable ones. JSON Web Key (JWK) format was not included in the blocklist. The vulnerable code pattern in versions up through 2.10.1 looked like this:
def prepare_key(self, key: str | bytes) -> bytes: key_bytes = force_bytes(key) if is_pem_format(key_bytes) or is_ssh_key(key_bytes): raise InvalidKeyError(...) return key_bytes
A JWK JSON string, such as {"kty":"RSA","n":"...","e":"AQAB"}, passes straight through both checks. Since HMAC algorithms accept arbitrary byte strings as secrets, the raw text of the JWK document is silently accepted as a valid HMAC key.
Attack Prerequisites
Exploitation requires two conditions to be simultaneously true in the target application:
| Condition | Description |
|---|---|
| Mixed algorithm families | The verifier passes both a symmetric algorithm (e.g., HS256) and an asymmetric algorithm (e.g., RS256) in the algorithms list when calling decode() |
| Raw JWK as key argument | The verifier supplies the public key as a raw JWK JSON string via the key parameter instead of using a PEM encoded key or a dedicated key object |
The CVSS vector reflects this: Attack Complexity is rated High (AC:H) because both conditions must hold. But when they do, the Confidentiality and Integrity impacts are both High, meaning the attacker achieves full token forgery.
Attack Flow
The attack follows the classic JWT algorithm confusion pattern, adapted for the JWK format gap:
-
Obtain the public key in JWK format. The attacker retrieves the issuer's public key from a publicly accessible endpoint such as
.well-known/jwks.json. This key is public by design and freely available. -
Craft a malicious token header. The attacker creates a JWT with the header
{"alg":"HS256","typ":"JWT"}, switching the algorithm from the expected asymmetric algorithm (e.g., RS256) to HMAC. -
Set arbitrary claims. The attacker constructs any payload they wish, such as
{"sub":"alice","admin":true}, to impersonate any identity or assume any role. -
Sign with HMAC using the JWK JSON as the secret. The attacker computes the HMAC SHA256 signature using the raw JWK JSON text as the symmetric secret key. Since the verifier will use the same JWK text as its "key" argument, the signatures will match.
-
Submit the forged token. When the vulnerable verifier processes this token with a call like
decode(token, key=rsa_jwk_json, algorithms=["HS256","RS256"]), it sees thealgheader isHS256, routes verification toHMACAlgorithm, passes the JWK string throughprepare_key(which does not reject it), and validates the attacker's forged signature as legitimate.
The result is a complete authentication bypass. The attacker can impersonate any user, escalate privileges, or access any protected resource that trusts the JWT verifier.
Relationship to CVE-2022-29217
This vulnerability is a direct descendant of CVE-2022-29217 (GHSA-ffqj-6fqr-9h24), which was fixed in PyJWT 2.4.0. That earlier fix addressed the same algorithm confusion attack class but only for PEM and SSH key formats. The comparison is instructive:
| Attribute | CVE-2022-29217 | CVE-2026-48526 |
|---|---|---|
| Advisory | GHSA-ffqj-6fqr-9h24 | GHSA-xgmm-8j9v-c9wx |
| Key format bypassed | PEM, SSH | JWK JSON |
| Fixed version | 2.4.0 | 2.13.0 |
| CVSS | 7.5 | 7.4 |
| Root cause | HMACAlgorithm.prepare_key insufficient validation | Same class, different format gap |
Both vulnerabilities share the same fundamental root cause: HMACAlgorithm.prepare_key relied on an enumerated blocklist of key formats rather than a whitelist of acceptable HMAC key types. Any format not explicitly blocked became an attack vector. The JWK gap persisted across every release from 2.4.0 through 2.10.1, meaning applications that patched CVE-2022-29217 remained vulnerable to this variant for over two years.
CVSS v3.1 Vector Breakdown
The full vector is CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N, which breaks down as follows:
| Metric | Value | Rationale |
|---|---|---|
| Attack Vector | Network (AV:N) | Exploitable over the network via crafted JWT |
| Attack Complexity | High (AC:H) | Requires mixed algorithm families and JWK key format |
| Privileges Required | None (PR:N) | No authentication needed |
| User Interaction | None (UI:N) | No user action required |
| Scope | Unchanged (S:U) | Impact confined to the vulnerable component |
| Confidentiality | High (C:H) | Full access to protected data |
| Integrity | High (I:H) | Token forgery enables arbitrary identity impersonation |
| Availability | None (A:N) | No direct availability impact |
The NVD assigns both CWE-287 (Improper Authentication) and CWE-347 (Improper Verification of Cryptographic Signature), reflecting that this is simultaneously an authentication logic error and a cryptographic verification failure.
Patch Information
A public patch for CVE-2026-48526 is available in PyJWT version 2.13.0, released on May 21, 2026. The fix was committed by maintainer José Padilla in commit 95791b1 and tagged in 7144e45, tracked under GitHub Security Advisory GHSA-xgmm-8j9v-c9wx.
The patch adds a new guard in HMACAlgorithm.prepare_key after the existing PEM/SSH checks. It strips leading whitespace from the key bytes, checks if the result starts with {, and if so, attempts to parse it as JSON. If parsing succeeds and the resulting dictionary contains a "kty" key (the mandatory JWK field per RFC 7517), the method raises an InvalidKeyError:
# Defense against algorithm-confusion attacks: an attacker with # control over the token header can force this code path by setting # alg=HS*, and HMACAlgorithm is the only algorithm that accepts # arbitrary bytes as a valid secret. stripped = key_bytes.lstrip() if stripped.startswith(b"{"): try: jwk_obj = json.loads(key_bytes) except ValueError: jwk_obj = None if isinstance(jwk_obj, dict) and "kty" in jwk_obj: raise InvalidKeyError( "The specified key looks like a JWK and should not be " "used directly as an HMAC secret. Load it via " "PyJWK / HMACAlgorithm.from_jwk first." )
This logic is deliberately narrow: JSON strings that do not contain a "kty" field are still accepted as HMAC secrets, because a JSON shaped string without JWK structure is a legitimate (if unusual) secret. This avoids false positives.
In addition to the JWK guard, the same commit also hardens prepare_key by outright rejecting empty HMAC keys (len(key_bytes) == 0) with an InvalidKeyError, rather than the previous behavior of merely emitting a warning. This catches the common misconfiguration of os.getenv("JWT_SECRET", "") falling through silently.
The fix was accompanied by comprehensive test coverage, including parametrized tests that feed RSA, EC, OKP, and HMAC JWK files into prepare_key and assert they are all rejected, as well as a negative test ensuring plain JSON strings without "kty" remain accepted.
To upgrade:
pip install --upgrade PyJWT==2.13.0
Version 2.13.0 also includes four additional security fixes that address related attack surfaces:
| Advisory | Issue |
|---|---|
| GHSA-jq35-7prp-9v3f | Algorithm allow list bypass via PyJWK/PyJWKClient |
| GHSA-993g-76c3-p5m4 | URI scheme injection in PyJWKClient |
| GHSA-fhv5-28vv-h8m8 | JWK Set cache cleared on transient fetch errors |
| GHSA-w7vc-732c-9m39 | Unauthenticated DoS via b64=false base64 decode |
Organizations should upgrade to 2.13.0 not only for the CVE-2026-48526 fix but also to receive these complementary hardening measures.
Post Upgrade Verification Checklist
After upgrading, security teams should verify:
- All PyJWT dependencies are at version 2.13.0 or later.
- No
decode()calls mix symmetric and asymmetric algorithm families. - No
decode()calls pass raw JWK JSON strings as thekeyargument. - The
algorithmsparameter is always explicitly specified (never omitted or defaulted).
Affected Systems and Versions
The vulnerability affects PyJWT versions prior to 2.13.0. More specifically:
- Versions 2.4.0 through 2.10.1 are vulnerable to the JWK format bypass described in CVE-2026-48526. These versions contain the CVE-2022-29217 fix (blocking PEM and SSH keys) but lack the JWK guard.
- Versions prior to 2.4.0 are vulnerable to both the original PEM/SSH algorithm confusion (CVE-2022-29217) and the JWK variant.
- The vulnerability is only exploitable when the application is configured to accept both symmetric and asymmetric algorithms in the
algorithmslist and passes a raw JWK JSON string as thekeyargument todecode().
The fixed version is PyJWT 2.13.0.
Vendor Security History
PyJWT has experienced multiple algorithm related vulnerabilities over its lifetime, revealing a persistent architectural challenge in safely handling the JWT specification's algorithm flexibility:
| CVE | Year | Severity | Issue | Fixed Version |
|---|---|---|---|---|
| CVE-2022-29217 | 2022 | 7.5 High | Key confusion via non blocklisted PEM/SSH key formats | 2.4.0 |
| CVE-2025-45768 | 2025 | N/A | Weak encryption vulnerability in PyJWT v2.10.1 | Under investigation |
| CVE-2026-32597 | 2026 | N/A | Related to GHSA-752w advisory | 2.13.0 |
| CVE-2026-48526 | 2026 | 7.4 High | JWK JSON accepted as HMAC secret (algorithm confusion) | 2.13.0 |
The evolution from CVE-2022-29217 to CVE-2026-48526 demonstrates a pattern: the blocklist approach to key format validation creates a "whack a mole" dynamic where each new key format representation becomes a potential bypass. The 2.13.0 release, which addresses five security advisories simultaneously, suggests the maintainer has shifted toward a more systemic security posture. The inclusion of algorithm binding (GHSA-jq35-7prp-9v3f) in the same release is particularly notable, as it moves toward a type aware verification model rather than relying solely on format detection.
A parallel vulnerability in the Node.js ecosystem, CVE-2026-34950 in the fast-jwt library, confirms that algorithm confusion remains an active and recurring challenge across JWT implementations in multiple languages.
References
- NVD: CVE-2026-48526
- CVE Record: CVE-2026-48526
- GitHub Security Advisory: GHSA-xgmm-8j9v-c9wx
- PyJWT 2.13.0 Changelog
- PyJWT GitHub Repository
- PyJWT 2.12.1 to 2.13.0 Comparison
- PyJWT Releases
- PyJWT algorithms.py Source
- CVE-2022-29217 Advisory: GHSA-ffqj-6fqr-9h24
- WorkOS: JWT Algorithm Confusion Attacks
- PortSwigger: Algorithm Confusion Attacks
- VulnCheck: 2026 Routinely Targeted Vulnerabilities
- fast-jwt Algorithm Confusion Re-Enabled (CVE-2026-34950)
- SentinelOne: CVE-2022-29217
- Auth0: How to Handle JWTs in Python
- PyJWT 2.13.0 Documentation



