python3-pyjwt: fix CVE-2026-102269

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

[1] https://github.com/jpadilla/pyjwt/commit/e6f48401001609a8f99e71fcaf355fb895d508a8
[2] https://github.com/jpadilla/pyjwt/security/advisories/GHSA-hxm8-2xgr-2p9m

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 e93a40cd9d
commit fb76c963d9
2 files changed
+134

No files matched your search

@@ -0,0 +1,133 @@
From e6f48401001609a8f99e71fcaf355fb895d508a8 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 09:46:01 -0400
Subject: [PATCH] Reject non-canonical compact JWS segments
CVE: CVE-2026-102269
Upstream-Status: Backport [https://github.com/jpadilla/pyjwt/commit/e6f48401001609a8f99e71fcaf355fb895d508a8]
(cherry picked from commit e6f48401001609a8f99e71fcaf355fb895d508a8)
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
---
jwt/api_jws.py | 35 +++++++++++++++++++++++------------
tests/test_api_jws.py | 11 +++++++++++
tests/test_compressed_jwt.py | 8 ++++----
3 files changed, 38 insertions(+), 16 deletions(-)
diff --git a/jwt/api_jws.py b/jwt/api_jws.py
index 5a91bce..2ee2f22 100644
--- a/jwt/api_jws.py
+++ b/jwt/api_jws.py
@@ -28,6 +28,9 @@ if TYPE_CHECKING:
from .types import SigOptions
_ALGORITHM_UNSET = object()
+_BASE64URL_ALPHABET = (
+ b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
+)
class PyJWS:
@@ -291,6 +294,23 @@ class PyJWS:
return headers
+ @staticmethod
+ def _decode_base64url_segment(segment: bytes, name: str) -> bytes:
+ if len(segment) % 4 == 1 or any(
+ character not in _BASE64URL_ALPHABET for character in segment
+ ):
+ raise DecodeError(f"Invalid {name} padding")
+
+ try:
+ decoded = base64url_decode(segment)
+ except (TypeError, binascii.Error) as err:
+ raise DecodeError(f"Invalid {name} padding") from err
+
+ if base64url_encode(decoded) != segment:
+ raise DecodeError(f"Invalid {name} padding")
+
+ return decoded
+
def _load(self, jwt: str | bytes) -> tuple[bytes, bytes, dict[str, Any], bytes]:
if isinstance(jwt, str):
jwt = jwt.encode("utf-8")
@@ -304,10 +324,7 @@ class PyJWS:
except ValueError as err:
raise DecodeError("Not enough segments") from err
- try:
- header_data = base64url_decode(header_segment)
- except (TypeError, binascii.Error) as err:
- raise DecodeError("Invalid header padding") from err
+ header_data = self._decode_base64url_segment(header_segment, "header")
try:
header: dict[str, Any] = json.loads(header_data)
@@ -331,15 +348,9 @@ class PyJWS:
)
payload = b""
else:
- try:
- payload = base64url_decode(payload_segment)
- except (TypeError, binascii.Error) as err:
- raise DecodeError("Invalid payload padding") from err
+ payload = self._decode_base64url_segment(payload_segment, "payload")
- try:
- signature = base64url_decode(crypto_segment)
- except (TypeError, binascii.Error) as err:
- raise DecodeError("Invalid crypto padding") from err
+ signature = self._decode_base64url_segment(crypto_segment, "crypto")
return (payload, signing_input, header, signature)
diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py
index 01de813..7e0a97b 100644
--- a/tests/test_api_jws.py
+++ b/tests/test_api_jws.py
@@ -658,6 +658,17 @@ class TestJWS:
assert "Invalid crypto padding" in str(exc.value)
+ def test_decode_rejects_non_canonical_crypto_segment(
+ self, jws: PyJWS, payload: bytes
+ ) -> None:
+ secret = "a" * 32
+ token = jws.encode(payload, secret, algorithm="HS256")
+ header, encoded_payload, signature = token.split(".")
+ mutated_token = ".".join((header, encoded_payload, f"{signature}!!!!"))
+
+ with pytest.raises(DecodeError, match="Invalid crypto padding"):
+ jws.decode(mutated_token, secret, algorithms=["HS256"])
+
def test_decode_with_algo_none_should_fail(
self, jws: PyJWS, payload: bytes
) -> None:
diff --git a/tests/test_compressed_jwt.py b/tests/test_compressed_jwt.py
index 1968cb4..51c61a8 100644
--- a/tests/test_compressed_jwt.py
+++ b/tests/test_compressed_jwt.py
@@ -22,8 +22,8 @@ def test_decodes_complete_valid_jwt_with_compressed_payload() -> None:
# Buffer.from(pako.deflateRaw('{"hello": "world"}')).toString('base64')
example_jwt = (
b"eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9"
- b".q1bKSM3JyVeyUlAqzy/KSVGqBQA="
- b".08wHYeuh1rJXmcBcMrz6NxmbxAnCQp2rGTKfRNIkxiw="
+ b".q1bKSM3JyVeyUlAqzy_KSVGqBQA"
+ b".AAn1elCJC5MQCYFwTwa2tjjtyLgqLUVU-Y1vFBsU8jo"
)
decoded = CompressedPyJWT().decode_complete(
example_jwt, example_secret, algorithms=["HS256"]
@@ -33,7 +33,7 @@ def test_decodes_complete_valid_jwt_with_compressed_payload() -> None:
"header": {"alg": "HS256", "typ": "JWT"},
"payload": example_payload,
"signature": (
- b"\xd3\xcc\x07a\xeb\xa1\xd6\xb2W\x99\xc0\\2\xbc\xfa7"
- b"\x19\x9b\xc4\t\xc2B\x9d\xab\x192\x9fD\xd2$\xc6,"
+ b"\x00\t\xf5zP\x89\x0b\x93\x10\t\x81pO\x06\xb6\xb68"
+ b"\xed\xc8\xb8*-ET\xf9\x8do\x14\x1b\x14\xf2:"
),
}
--
2.35.6
@@ -12,6 +12,7 @@ SRC_URI += "file://CVE-2026-48522.patch \
file://CVE-2026-48526.patch \
file://CVE-2026-103001.patch \
file://CVE-2026-102274.patch \
file://CVE-2026-102269.patch \
"
SRC_URI[sha256sum] = "c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"