From ff542029d6865add176b3d4b650d8f1dc514ac85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Mon, 3 Aug 2026 03:20:44 -0700 Subject: [PATCH] PyJWKClient: reject non-HTTP(S) JWKS URIs Restrict JWKS retrieval to HTTP and HTTPS. urllib otherwise accepts additional schemes such as file, FTP and data, allowing attacker-influenced URLs to reach unintended resources. CVE: CVE-2026-48522 Upstream-Status: Backport [https://github.com/jpadilla/pyjwt/commit/95791b1759b8aa4f2203575d344d5c78564cdc81] Backport Changes: - Extracted only the CVE-2026-48522 URI-scheme validation and regression tests from the bundled upstream 2.13.0 commit. The other requested CVE fixes are carried as separate patches. - Adapted the hunk context to the PyJWT 2.8.0 constructor and typing imports. - Omitted upstream test comments while retaining the same URI cases and assertions. - Omitted the 2.13.0 version and changelog updates, CVE-2026-48523 (which does not affect 2.8.0), and unrelated hardening from the bundled commit. (cherry picked from commit 95791b1759b8aa4f2203575d344d5c78564cdc81) Signed-off-by: Hetvi Thakar --- jwt/jwks_client.py | 11 +++++++++++ tests/test_jwks_client.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py index f19b10a..18de342 100644 --- a/jwt/jwks_client.py +++ b/jwt/jwks_client.py @@ -4,6 +4,7 @@ from functools import lru_cache from ssl import SSLContext from typing import Any, Dict, List, Optional from urllib.error import URLError +from urllib.parse import urlparse from .api_jwk import PyJWK, PyJWKSet from .api_jwt import decode_complete as decode_token @@ -25,6 +26,16 @@ class PyJWKClient: ): if headers is None: headers = {} + # urllib's default OpenerDirector also handles file://, ftp://, and + # data: URIs. Reject anything that isn't http(s) eagerly so a caller + # passing an attacker-influenced URL (e.g. taken from a `jku` token + # header) can't read local files or reach other unintended schemes. + scheme = urlparse(uri).scheme.lower() + if scheme not in ("http", "https"): + raise PyJWKClientError( + f"Invalid JWKS URI scheme {scheme!r}: only 'http' and 'https' " + f"are supported." + ) self.uri = uri self.jwk_set_cache: Optional[JWKSetCache] = None self.headers = headers diff --git a/tests/test_jwks_client.py b/tests/test_jwks_client.py index c3951ea..d4bdd35 100644 --- a/tests/test_jwks_client.py +++ b/tests/test_jwks_client.py @@ -327,6 +327,31 @@ class TestPyJWKClient: jwks_client = PyJWKClient(url, lifespan=-1) assert jwks_client is None + @pytest.mark.parametrize( + "uri", + [ + "file:///etc/passwd", + "ftp://example.org/keys.json", + 'data:application/json,{"keys":[]}', + "/etc/passwd", + "ldap://internal.test/jwks", + ], + ) + def test_pyjwkclient_rejects_non_http_schemes(self, uri: str) -> None: + with pytest.raises(PyJWKClientError, match="Invalid JWKS URI scheme"): + PyJWKClient(uri) + + @pytest.mark.parametrize( + "uri", + [ + "http://localhost/jwks.json", + "https://example.test/jwks.json", + "HTTPS://Example.Test/jwks.json", + ], + ) + def test_pyjwkclient_accepts_http_https_schemes(self, uri: str) -> None: + PyJWKClient(uri) + def test_get_jwt_set_timeout(self): url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" jwks_client = PyJWKClient(url, timeout=5)