From 91ac94bdc85d24c6d35a5f6cdd58eb8c6c4df0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Mon, 3 Aug 2026 03:21:30 -0700 Subject: [PATCH] api_jws: reject non-empty detached payload segments For b64=false tokens, reject a non-empty compact payload segment before Base64URL decoding. The segment is unused when detached_payload is supplied, so decoding attacker-controlled data only consumes CPU and memory. CVE: CVE-2026-48525 Upstream-Status: Backport [https://github.com/jpadilla/pyjwt/commit/95791b1759b8aa4f2203575d344d5c78564cdc81] Backport Changes: - Extracted only the CVE-2026-48525 detached-payload segment check and regression test from the bundled upstream 2.13.0 commit. The other requested CVE fixes are carried as separate patches. - Adapted the hunk and test locations to PyJWT 2.8.0, used module-level hashlib and hmac imports, and shortened explanatory comments without changing the tested behavior. - Excluded the separate RFC 7797 b64/crit hardening bundled upstream. - Omitted the 2.13.0 version and changelog updates, CVE-2026-48523 (which does not affect 2.8.0), and unrelated hardening from the bundled commit. (cherry picked from commit 95791b1759b8aa4f2203575d344d5c78564cdc81) Signed-off-by: Hetvi Thakar --- jwt/api_jws.py | 17 +++++++++++++---- tests/test_api_jws.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/jwt/api_jws.py b/jwt/api_jws.py index 1750442..0d5ab2b 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -274,10 +274,19 @@ class PyJWS: if not isinstance(header, dict): raise DecodeError("Invalid header string: must be a json object") - try: - payload = base64url_decode(payload_segment) - except (TypeError, binascii.Error) as err: - raise DecodeError("Invalid payload padding") from err + if header.get("b64", True) is False: + # Detached compact serialization requires an empty payload + # segment. Reject it before decoding attacker-controlled data. + if payload_segment: + raise DecodeError( + "Payload segment must be empty when 'b64' is false." + ) + payload = b"" + else: + try: + payload = base64url_decode(payload_segment) + except (TypeError, binascii.Error) as err: + raise DecodeError("Invalid payload padding") from err try: signature = base64url_decode(crypto_segment) diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py index 434874b..29f84a7 100644 --- a/tests/test_api_jws.py +++ b/tests/test_api_jws.py @@ -1,3 +1,5 @@ +import hashlib +import hmac import json from decimal import Decimal @@ -11,7 +13,7 @@ from jwt.exceptions import ( InvalidSignatureError, InvalidTokenError, ) -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, no_crypto_required @@ -766,6 +768,36 @@ class TestJWS: assert "b64" not in msg_header_obj assert msg_payload + def test_decode_b64_false_rejects_non_empty_payload_segment( + self, jws: PyJWS, payload: bytes + ) -> None: + secret = "secret" + header = { + "typ": "JWT", + "alg": "HS256", + "b64": False, + "crit": ["b64"], + } + encoded_header = base64url_encode( + json.dumps(header, separators=(",", ":")).encode() + ) + attacker_segment = b"A" * 1024 + signing_input = b".".join([encoded_header, payload]) + signature = hmac.new( + secret.encode(), signing_input, hashlib.sha256 + ).digest() + token = b".".join( + [encoded_header, attacker_segment, base64url_encode(signature)] + ).decode() + + with pytest.raises(DecodeError, match="Payload segment must be empty"): + jws.decode( + token, + secret, + algorithms=["HS256"], + detached_payload=payload, + ) + def test_decode_detached_content_without_proper_argument(self, jws): example_jws = ( "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2V9"