From 5fde08a6cf906aa7698de2d6391d88b73006b17b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?= <jpadilla@users.noreply.github.com>
Date: Tue, 22 Sep 2026 11:44:19 -0400
Subject: [PATCH] fix: normalize recursive JWT payload errors

---
 jwt/api_jwt.py            |  2 +-
 tests/test_api_jwt.py     | 18 +++++++++++++++++-
 tests/test_jwks_client.py | 21 +++++++++++++++++++++
 3 files changed, 39 insertions(+), 2 deletions(-)

Index: pyjwt-2.12.1/jwt/api_jwt.py
===================================================================
--- pyjwt-2.12.1.orig/jwt/api_jwt.py
+++ pyjwt-2.12.1/jwt/api_jwt.py
@@ -294,7 +294,7 @@ class PyJWT:
         """
         try:
             payload: dict[str, Any] = json.loads(decoded["payload"])
-        except ValueError as e:
+        except (ValueError, RecursionError) as e:
             raise DecodeError(f"Invalid payload string: {e}") from e
         if not isinstance(payload, dict):
             raise DecodeError("Invalid payload string: must be a json object")
Index: pyjwt-2.12.1/tests/test_api_jwt.py
===================================================================
--- pyjwt-2.12.1.orig/tests/test_api_jwt.py
+++ pyjwt-2.12.1/tests/test_api_jwt.py
@@ -6,6 +6,7 @@ from decimal import Decimal
 
 import pytest
 
+import jwt as pyjwt
 from jwt.types import Options
 from jwt.api_jwk import PyJWK
 from jwt.api_jwt import PyJWT
@@ -20,7 +21,7 @@ from jwt.exceptions import (
     InvalidSubjectError,
     MissingRequiredClaimError,
 )
-from jwt.utils import base64url_decode
+from jwt.utils import base64url_decode, base64url_encode
 from jwt.warnings import RemovedInPyjwt3Warning
 
 from .utils import crypto_required, key_path, utc_timestamp
@@ -121,6 +122,21 @@ class TestJWT:
 
         assert "Invalid payload string" in str(exc.value)
 
+    def test_decode_deeply_nested_payload_throws_decode_error(self) -> None:
+        nested_payload = b"[" * 100_000 + b"]" * 100_000
+        token = ".".join(
+            (
+                "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
+                base64url_encode(nested_payload).decode(),
+                "",
+            )
+        )
+
+        with pytest.raises(DecodeError, match="Invalid payload") as exc:
+            pyjwt.decode(token, options={"verify_signature": False})
+
+        assert isinstance(exc.value.__cause__, RecursionError)
+
     def test_decode_with_non_mapping_payload_throws_exception(self, jwt: PyJWT) -> None:
         secret = "secret"
         example_jwt = (
Index: pyjwt-2.12.1/tests/test_jwks_client.py
===================================================================
--- pyjwt-2.12.1.orig/tests/test_jwks_client.py
+++ pyjwt-2.12.1/tests/test_jwks_client.py
@@ -13,7 +13,9 @@ import pytest
 import jwt
 from jwt import PyJWKClient
 from jwt.api_jwk import PyJWK
-from jwt.exceptions import PyJWKClientConnectionError, PyJWKClientError
+from jwt.exceptions import DecodeError, PyJWKClientConnectionError, PyJWKClientError
+
+from jwt.utils import base64url_encode
 
 from .utils import crypto_required
 
@@ -236,6 +238,25 @@ class TestPyJWKClient:
             "gty": "client-credentials",
         }
 
+    def test_get_signing_key_from_jwt_rejects_deeply_nested_payload(self) -> None:
+        nested_payload = b"[" * 100_000 + b"]" * 100_000
+        header = b'{"alg":"RS256","kid":"test-key"}'
+        token = ".".join(
+            (
+                base64url_encode(header).decode(),
+                base64url_encode(nested_payload).decode(),
+                "",
+            )
+        )
+
+        with mocked_success_response(RESPONSE_DATA_WITH_MATCHING_KID) as open_mock:
+            jwks_client = PyJWKClient("https://example.test/.well-known/jwks.json")
+            with pytest.raises(DecodeError, match="Invalid payload") as exc:
+                jwks_client.get_signing_key_from_jwt(token)
+
+        assert isinstance(exc.value.__cause__, RecursionError)
+        assert open_mock.call_count == 0
+
     def test_get_jwk_set_caches_result(self) -> None:
         url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"
 
