new file mode 100644
@@ -0,0 +1,223 @@
+From 67ef104edd43a60ba87db0740364778eae796fe2 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Jos=C3=A9=20Padilla?= <jpadilla@users.noreply.github.com>
+Date: Wed, 14 Oct 2026 02:41:53 -0700
+Subject: [PATCH] Reject redirects in PyJWKClient fetches
+
+CVE: CVE-2026-102267
+Upstream-Status: Backport [https://github.com/jpadilla/pyjwt/commit/0a795b8e1f6ef08f634aa7086fc41cc6d5ce3e56]
+
+Backport Changes:
+- Used the existing typing.List import for the handlers annotation.
+- Omitted the mocked_http_error_response opener-mock conversion because
+ that helper is absent from the PyJWT 2.8.0 test suite.
+- Adapted the two SSL-context tests from live requests to opener mocks;
+ the 2.8.0 success test needs a local ssl_ctx and an explicit mocked
+ get_jwk_set call, and its failure test needs the certificate-error mock.
+
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ jwt/jwks_client.py | 21 +++++++--
+ tests/test_jwks_client.py | 96 ++++++++++++++++++++++++++++++++++-----
+ 2 files changed, 103 insertions(+), 14 deletions(-)
+
+diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py
+index 0e7d226..ab601a4 100644
+--- a/jwt/jwks_client.py
++++ b/jwt/jwks_client.py
+@@ -12,6 +12,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,
+@@ -61,9 +74,11 @@ class PyJWKClient:
+ def fetch_data(self) -> Any:
+ 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:
+ raise PyJWKClientConnectionError(
+diff --git a/tests/test_jwks_client.py b/tests/test_jwks_client.py
+index 4a836f2..b3f2555 100644
+--- a/tests/test_jwks_client.py
++++ b/tests/test_jwks_client.py
+@@ -1,7 +1,9 @@
+ import contextlib
+ import json
+ import ssl
++import threading
+ import time
++from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+ from unittest import mock
+ from urllib.error import URLError
+
+@@ -9,6 +11,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
+
+@@ -47,7 +50,10 @@ RESPONSE_DATA_NO_MATCHING_KID = {
+
+ @contextlib.contextmanager
+ def mocked_success_response(data):
+- 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()
+@@ -58,7 +64,10 @@ def mocked_success_response(data):
+
+ @contextlib.contextmanager
+ def mocked_failed_response():
+- 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
+
+@@ -67,7 +76,10 @@ def mocked_failed_response():
+ def mocked_first_call_wrong_kid_second_call_correct_kid(
+ response_data_one, response_data_two
+ ):
+- 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()
+@@ -81,13 +93,60 @@ def mocked_first_call_wrong_kid_second_call_correct_kid(
+
+ @contextlib.contextmanager
+ def mocked_timeout():
+- 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
+
+
+ @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):
+ url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
+
+@@ -372,9 +431,22 @@ class TestPyJWKClient:
+
+ def test_get_jwt_set_sslcontext_default(self):
+ url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
+- jwks_client = PyJWKClient(url, ssl_context=ssl.create_default_context())
+-
+- jwk_set = jwks_client.get_jwk_set()
++ ssl_ctx = ssl.create_default_context()
++ jwks_client = PyJWKClient(url, ssl_context=ssl_ctx)
++
++ 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()
++ 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
+
+@@ -384,7 +456,9 @@ class TestPyJWKClient:
+ url, ssl_context=ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
+ )
+
+- with pytest.raises(PyJWKClientError):
+- jwks_client.get_jwk_set()
+-
+- assert "Failed to get an expected error"
++ 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):
++ jwks_client.get_jwk_set()
+--
+2.35.6
@@ -13,6 +13,7 @@ SRC_URI += " \
file://CVE-2026-48526.patch \
file://CVE-2026-102269_p1.patch \
file://CVE-2026-102269_p2.patch \
+ file://CVE-2026-102267.patch \
"
SRC_URI[sha256sum] = "57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"