new file mode 100644
@@ -0,0 +1,223 @@
+From 0a795b8e1f6ef08f634aa7086fc41cc6d5ce3e56 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Jos=C3=A9=20Padilla?= <jpadilla@users.noreply.github.com>
+Date: Wed, 9 Sep 2026 20:20:46 -0400
+Subject: [PATCH] Reject redirects in PyJWKClient fetches
+
+CVE: CVE-2026-102267
+Upstream-Status: Backport [https://github.com/jpadilla/pyjwt/commit/0a795b8e1f6ef08f634aa7086fc41cc6d5ce3e56]
+
+(cherry picked from commit 0a795b8e1f6ef08f634aa7086fc41cc6d5ce3e56)
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ jwt/jwks_client.py | 21 +++++++--
+ tests/test_jwks_client.py | 91 ++++++++++++++++++++++++++++++++++-----
+ 2 files changed, 99 insertions(+), 13 deletions(-)
+
+diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py
+index 41f333a..cfc9003 100644
+--- a/jwt/jwks_client.py
++++ b/jwt/jwks_client.py
+@@ -14,6 +14,19 @@ from .exceptions import PyJWKClientConnectionError, PyJWKClientError
+ from .jwk_set_cache import JWKSetCache
+
+
++class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
++ def redirect_request(
++ self,
++ req: urllib.request.Request,
++ fp: Any,
++ code: int,
++ msg: str,
++ headers: Any,
++ newurl: str,
++ ) -> None:
++ return None
++
++
+ class PyJWKClient:
+ def __init__(
+ self,
+@@ -115,9 +128,11 @@ class PyJWKClient:
+ """
+ try:
+ r = urllib.request.Request(url=self.uri, headers=self.headers)
+- with urllib.request.urlopen(
+- r, timeout=self.timeout, context=self.ssl_context
+- ) as response:
++ handlers: list[Any] = [_NoRedirectHandler()]
++ if self.ssl_context is not None:
++ handlers.append(urllib.request.HTTPSHandler(context=self.ssl_context))
++ opener = urllib.request.build_opener(*handlers)
++ with opener.open(r, timeout=self.timeout) as response:
+ jwk_set = json.load(response)
+ except (URLError, TimeoutError) as e:
+ if isinstance(e, HTTPError):
+diff --git a/tests/test_jwks_client.py b/tests/test_jwks_client.py
+index da68664..49471ac 100644
+--- a/tests/test_jwks_client.py
++++ b/tests/test_jwks_client.py
+@@ -2,8 +2,10 @@ import contextlib
+ import io
+ import json
+ import ssl
++import threading
+ import time
+ from collections.abc import Iterator
++from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+ from unittest import mock
+ from urllib.error import HTTPError, URLError
+ from email.message import Message
+@@ -12,6 +14,7 @@ import pytest
+
+ import jwt
+ from jwt import PyJWKClient
++from jwt.jwks_client import _NoRedirectHandler
+ from jwt.api_jwk import PyJWK
+ from jwt.exceptions import PyJWKClientConnectionError, PyJWKClientError
+
+@@ -50,7 +53,10 @@ RESPONSE_DATA_NO_MATCHING_KID = {
+
+ @contextlib.contextmanager
+ def mocked_success_response(data: object) -> Iterator[mock.Mock]:
+- with mock.patch("urllib.request.urlopen") as urlopen_mock:
++ with mock.patch("urllib.request.build_opener") as build_opener_mock:
++ opener = mock.Mock()
++ build_opener_mock.return_value = opener
++ urlopen_mock = opener.open
+ response = mock.Mock()
+ response.__enter__ = mock.Mock(return_value=response)
+ response.__exit__ = mock.Mock()
+@@ -61,7 +67,10 @@ def mocked_success_response(data: object) -> Iterator[mock.Mock]:
+
+ @contextlib.contextmanager
+ def mocked_failed_response() -> Iterator[mock.Mock]:
+- with mock.patch("urllib.request.urlopen") as urlopen_mock:
++ with mock.patch("urllib.request.build_opener") as build_opener_mock:
++ opener = mock.Mock()
++ build_opener_mock.return_value = opener
++ urlopen_mock = opener.open
+ urlopen_mock.side_effect = URLError("Fail to process the request.")
+ yield urlopen_mock
+
+@@ -70,7 +79,10 @@ def mocked_failed_response() -> Iterator[mock.Mock]:
+ def mocked_first_call_wrong_kid_second_call_correct_kid(
+ response_data_one: object, response_data_two: object
+ ) -> Iterator[mock.Mock]:
+- with mock.patch("urllib.request.urlopen") as urlopen_mock:
++ with mock.patch("urllib.request.build_opener") as build_opener_mock:
++ opener = mock.Mock()
++ build_opener_mock.return_value = opener
++ urlopen_mock = opener.open
+ response = mock.Mock()
+ response.__enter__ = mock.Mock(return_value=response)
+ response.__exit__ = mock.Mock()
+@@ -84,14 +96,20 @@ def mocked_first_call_wrong_kid_second_call_correct_kid(
+
+ @contextlib.contextmanager
+ def mocked_timeout() -> Iterator[mock.Mock]:
+- with mock.patch("urllib.request.urlopen") as urlopen_mock:
++ with mock.patch("urllib.request.build_opener") as build_opener_mock:
++ opener = mock.Mock()
++ build_opener_mock.return_value = opener
++ urlopen_mock = opener.open
+ urlopen_mock.side_effect = TimeoutError("timed out")
+ yield urlopen_mock
+
+
+ @contextlib.contextmanager
+ def mocked_http_error_response() -> Iterator[tuple[mock.Mock, HTTPError]]:
+- with mock.patch("urllib.request.urlopen") as urlopen_mock:
++ with mock.patch("urllib.request.build_opener") as build_opener_mock:
++ opener = mock.Mock()
++ build_opener_mock.return_value = opener
++ urlopen_mock = opener.open
+ http_error = HTTPError(
+ url="https://example.com",
+ code=401,
+@@ -105,6 +123,50 @@ def mocked_http_error_response() -> Iterator[tuple[mock.Mock, HTTPError]]:
+
+ @crypto_required
+ class TestPyJWKClient:
++ def test_fetch_data_rejects_redirect_without_contacting_destination(self) -> None:
++ destination_requests = 0
++
++ class RedirectHandler(BaseHTTPRequestHandler):
++ def do_GET(self) -> None: # noqa: N802 - stdlib handler API
++ nonlocal destination_requests
++ if self.path == "/start":
++ self.send_response(302)
++ self.send_header("Location", f"{base_url}/destination")
++ self.end_headers()
++ else:
++ destination_requests += 1
++ self.send_response(200)
++ self.end_headers()
++
++ def log_message(self, _format: str, *args: object) -> None:
++ return
++
++ server = ThreadingHTTPServer(("127.0.0.1", 0), RedirectHandler)
++ base_url = f"http://127.0.0.1:{server.server_port}"
++ thread = threading.Thread(target=server.serve_forever, daemon=True)
++ thread.start()
++ try:
++ client = PyJWKClient(
++ f"{base_url}/start", headers={"Authorization": "secret"}
++ )
++ with pytest.raises(PyJWKClientConnectionError):
++ client.fetch_data()
++ assert destination_requests == 0
++ finally:
++ server.shutdown()
++ server.server_close()
++ thread.join()
++
++ def test_redirect_handler_does_not_follow_redirects(self) -> None:
++ handler = _NoRedirectHandler()
++
++ assert (
++ handler.redirect_request(
++ mock.Mock(), mock.Mock(), 302, "Found", {}, "https://other.test"
++ )
++ is None
++ )
++
+ def test_fetch_data_forwards_headers_to_correct_url(self) -> None:
+ url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
+
+@@ -401,10 +463,19 @@ class TestPyJWKClient:
+ ssl_ctx = ssl.create_default_context()
+ jwks_client = PyJWKClient(url, ssl_context=ssl_ctx)
+
+- with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID) as mock_request:
++ with mock.patch("urllib.request.build_opener") as build_opener_mock:
++ opener = mock.Mock()
++ build_opener_mock.return_value = opener
++ response = mock.Mock()
++ response.__enter__ = mock.Mock(return_value=response)
++ response.__exit__ = mock.Mock()
++ response.read.return_value = json.dumps(RESPONSE_DATA_WITH_MATCHING_KID)
++ opener.open.return_value = response
+ jwk_set = jwks_client.get_jwk_set()
+- request_call = mock_request.call_args
+- assert request_call[1].get("context") is ssl_ctx
++ handlers = build_opener_mock.call_args.args
++ assert any(
++ getattr(handler, "_context", None) is ssl_ctx for handler in handlers
++ )
+
+ assert jwk_set is not None
+
+@@ -414,8 +485,8 @@ class TestPyJWKClient:
+ url, ssl_context=ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
+ )
+
+- with mock.patch("urllib.request.urlopen") as urlopen_mock:
+- urlopen_mock.side_effect = URLError(
++ with mock.patch("urllib.request.build_opener") as build_opener_mock:
++ build_opener_mock.return_value.open.side_effect = URLError(
+ ssl.SSLCertVerificationError("certificate verify failed")
+ )
+ with pytest.raises(PyJWKClientError):
+--
+2.35.6
@@ -13,6 +13,7 @@ SRC_URI += "file://CVE-2026-48522.patch \
file://CVE-2026-103001.patch \
file://CVE-2026-102274.patch \
file://CVE-2026-102269.patch \
+ file://CVE-2026-102267.patch \
"
SRC_URI[sha256sum] = "c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"