From 8b4e233a22206b34ec1186e912e75c0b2396ac07 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 21:49:21 -0400
Subject: [PATCH] fix: reject loader-accepted PEM variants

---
 jwt/utils.py             | 20 ++++++++++--------
 tests/test_algorithms.py | 44 +++++++++++++++++++++++++++++++++++++++-
 2 files changed, 55 insertions(+), 9 deletions(-)

diff --git a/jwt/utils.py b/jwt/utils.py
index 56e89bb77..914cb50d0 100644
--- a/jwt/utils.py
+++ b/jwt/utils.py
@@ -113,18 +113,22 @@ def raw_to_der_signature(raw_sig: bytes, curve: "EllipticCurve") -> bytes:
     b"X509 CRL",
 }
 
-_PEM_RE = re.compile(
-    b"----[- ]BEGIN ("
-    + b"|".join(_PEMS)
-    + b""")[- ]----\r?
-.+?\r?
-----[- ]END \\1[- ]----\r?\n?""",
-    re.DOTALL,
+# Accept PEM formatting variants that the cryptography loader accepts, while
+# retaining the explicit asymmetric-key marker check.
+_PEM_MARKER_RE = re.compile(
+    b"(?=(----[- ](BEGIN|END) (" + b"|".join(_PEMS) + b")[- ]----))"
 )
 
 
 def is_pem_format(key: bytes) -> bool:
-    return bool(_PEM_RE.search(key))
+    begin_markers: dict[bytes, int] = {}
+    for marker in _PEM_MARKER_RE.finditer(key):
+        marker_type, label = marker.group(2), marker.group(3)
+        if marker_type == b"BEGIN":
+            begin_markers[label] = marker.start(1) + len(marker.group(1))
+        elif label in begin_markers and begin_markers[label] < marker.start(1):
+            return True
+    return False
 
 
 # Based on https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b/src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46
diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py
index c854f7230..3d85a0cd3 100644
--- a/tests/test_algorithms.py
+++ b/tests/test_algorithms.py
@@ -1,6 +1,6 @@
 import base64
 import json
-from typing import Union, cast
+from typing import Callable, Union, cast
 
 import pytest
 
@@ -89,6 +89,48 @@ def test_hmac_should_throw_exception(self, key: str) -> None:
             with open(key_path(key)) as keyfile:
                 algo.prepare_key(keyfile.read())
 
+    @pytest.mark.parametrize(
+        "mutation",
+        [
+            lambda key: key.replace(b"-----END", b"\t-----END"),
+            lambda key: key.replace(b"\n", b"\r"),
+            lambda key: key.replace(b"\n", b""),
+        ],
+    )
+    def test_hmac_should_reject_loader_accepted_pem_mutations(
+        self, mutation: Callable[[bytes], bytes]
+    ) -> None:
+        algo = HMACAlgorithm(HMACAlgorithm.SHA256)
+        with open(key_path("testkey2_rsa.pub.pem"), "rb") as keyfile:
+            mutated_key = mutation(keyfile.read())
+
+        with pytest.raises(InvalidKeyError):
+            algo.prepare_key(mutated_key)
+
+    def test_hmac_should_accept_incomplete_pem_marker(self) -> None:
+        algo = HMACAlgorithm(HMACAlgorithm.SHA256)
+        key = b"-----BEGIN PUBLIC KEY-----" * 10000
+
+        assert algo.prepare_key(key) == key
+
+    def test_hmac_should_reject_a_later_pem_block(self) -> None:
+        algo = HMACAlgorithm(HMACAlgorithm.SHA256)
+        with open(key_path("testkey2_rsa.pub.pem"), "rb") as keyfile:
+            key = b"-----BEGIN PUBLIC KEY-----" + keyfile.read()
+
+        with pytest.raises(InvalidKeyError):
+            algo.prepare_key(key)
+
+    def test_hmac_should_reject_a_pem_block_after_an_overlapping_end_marker(
+        self,
+    ) -> None:
+        algo = HMACAlgorithm(HMACAlgorithm.SHA256)
+        with open(key_path("testkey2_rsa.pub.pem"), "rb") as keyfile:
+            key = b"-----END CERTIFICATE" + keyfile.read()
+
+        with pytest.raises(InvalidKeyError):
+            algo.prepare_key(key)
+
     def test_hmac_jwk_should_parse_and_verify(self) -> None:
         algo = HMACAlgorithm(HMACAlgorithm.SHA256)
 
