python3-pyjwt: fix CVE-2026-102267

This patch applies the upstream fix as referenced in [2], using the
commit shown in [1].

[1] https://github.com/jpadilla/pyjwt/commit/0a795b8e1f6ef08f634aa7086fc41cc6d5ce3e56
[2] https://github.com/jpadilla/pyjwt/security/advisories/GHSA-9v7f-9g4p-ffgj

Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Hetvi Thakar authored and Anuj Mittal committed 2026-10-28 12:31:35 +05:30
1 parent fb76c963d9
commit 6febd066de
2 files changed
+224

No files matched your search

@@ -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"