Version in base suite: 2.6.0-1 Base version: pyjwt_2.6.0-1 Target version: pyjwt_2.6.0-1+deb12u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/pyjwt/pyjwt_2.6.0-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/pyjwt/pyjwt_2.6.0-1+deb12u1.dsc changelog | 11 + patches/0001-Merge-commit-from-fork.patch | 210 ++++++++++++++++++++++++++++++ patches/series | 1 3 files changed, 222 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpy6n8urhe/pyjwt_2.6.0-1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpy6n8urhe/pyjwt_2.6.0-1+deb12u1.dsc: no acceptable signature found diff -Nru pyjwt-2.6.0/debian/changelog pyjwt-2.6.0/debian/changelog --- pyjwt-2.6.0/debian/changelog 2023-01-03 02:04:35.000000000 +0000 +++ pyjwt-2.6.0/debian/changelog 2026-04-28 14:47:59.000000000 +0000 @@ -1,3 +1,14 @@ +pyjwt (2.6.0-1+deb12u1) bookworm-security; urgency=high + + * Team upload. + * Fix CVE-2026-32597: PyJWT did not validate the crit (Critical) Header + Parameter defined in RFC 7515 §4.1.11. When a JWS token contains a crit + array listing extensions that PyJWT does not understand, the library + accepts the token instead of rejecting it. This violates the MUST + requirement in the RFC. (Closes: #1130662) + + -- Jochen Sprickerhof Tue, 28 Apr 2026 16:47:59 +0200 + pyjwt (2.6.0-1) unstable; urgency=medium * New upstream version 2.6.0 diff -Nru pyjwt-2.6.0/debian/patches/0001-Merge-commit-from-fork.patch pyjwt-2.6.0/debian/patches/0001-Merge-commit-from-fork.patch --- pyjwt-2.6.0/debian/patches/0001-Merge-commit-from-fork.patch 1970-01-01 00:00:00.000000000 +0000 +++ pyjwt-2.6.0/debian/patches/0001-Merge-commit-from-fork.patch 2026-04-28 14:47:59.000000000 +0000 @@ -0,0 +1,210 @@ +From: =?utf-8?q?Jos=C3=A9_Padilla?= +Date: Thu, 12 Mar 2026 12:46:08 -0400 +Subject: Merge commit from fork +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +Co-authored-by: José Padilla + + +Origin: backport, https://github.com/jpadilla/pyjwt/commit/051ea341b5573fe3edcd53042f347929b92c2b92 +--- + jwt/api_jws.py | 27 +++++++++++++- + tests/test_api_jws.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++ + tests/test_api_jwt.py | 18 +++++++++ + 3 files changed, 143 insertions(+), 2 deletions(-) + +diff --git a/jwt/api_jws.py b/jwt/api_jws.py +index ab8490f..51cf889 100644 +--- a/jwt/api_jws.py ++++ b/jwt/api_jws.py +@@ -121,7 +121,7 @@ class PyJWS: + header: dict[str, Any] = {"typ": self.header_typ, "alg": algorithm_} + + if headers: +- self._validate_headers(headers) ++ self._validate_headers(headers, encoding=True) + header.update(headers) + + if not header["typ"]: +@@ -190,6 +190,8 @@ class PyJWS: + + payload, signing_input, header, signature = self._load(jwt) + ++ self._validate_headers(header) ++ + if header.get("b64", True) is False: + if detached_payload is None: + raise DecodeError( +@@ -300,14 +302,35 @@ class PyJWS: + if not alg_obj.verify(signing_input, key, signature): + raise InvalidSignatureError("Signature verification failed") + +- def _validate_headers(self, headers: dict[str, Any]) -> None: ++ # Extensions that PyJWT actually understands and supports ++ _supported_crit: set[str] = {"b64"} ++ ++ def _validate_headers( ++ self, headers: dict[str, Any], *, encoding: bool = False ++ ) -> None: + if "kid" in headers: + self._validate_kid(headers["kid"]) ++ if not encoding and "crit" in headers: ++ self._validate_crit(headers) + + def _validate_kid(self, kid: str) -> None: + if not isinstance(kid, str): + raise InvalidTokenError("Key ID header parameter must be a string") + ++ def _validate_crit(self, headers: dict[str, Any]) -> None: ++ crit = headers["crit"] ++ if not isinstance(crit, list) or len(crit) == 0: ++ raise InvalidTokenError("Invalid 'crit' header: must be a non-empty list") ++ for ext in crit: ++ if not isinstance(ext, str): ++ raise InvalidTokenError("Invalid 'crit' header: values must be strings") ++ if ext not in self._supported_crit: ++ raise InvalidTokenError(f"Unsupported critical extension: {ext}") ++ if ext not in headers: ++ raise InvalidTokenError( ++ f"Critical extension '{ext}' is missing from headers" ++ ) ++ + + _jws_global_obj = PyJWS() + encode = _jws_global_obj.encode +diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py +index cfbbe21..1c306ab 100644 +--- a/tests/test_api_jws.py ++++ b/tests/test_api_jws.py +@@ -805,3 +805,103 @@ class TestJWS: + ) + assert len(record) == 1 + assert "foo" in str(record[0].message) ++ ++ def test_decode_rejects_unknown_crit_extension( ++ self, jws: PyJWS, payload: bytes ++ ) -> None: ++ secret = "secret" ++ token = jws.encode( ++ payload, ++ secret, ++ algorithm="HS256", ++ headers={"crit": ["x-custom-policy"], "x-custom-policy": "require-mfa"}, ++ ) ++ ++ with pytest.raises(InvalidTokenError, match="Unsupported critical extension"): ++ jws.decode(token, secret, algorithms=["HS256"]) ++ ++ def test_decode_rejects_empty_crit(self, jws: PyJWS, payload: bytes) -> None: ++ secret = "secret" ++ token = jws.encode( ++ payload, ++ secret, ++ algorithm="HS256", ++ headers={"crit": []}, ++ ) ++ ++ with pytest.raises(InvalidTokenError, match="must be a non-empty list"): ++ jws.decode(token, secret, algorithms=["HS256"]) ++ ++ def test_decode_rejects_non_list_crit(self, jws: PyJWS, payload: bytes) -> None: ++ secret = "secret" ++ token = jws.encode( ++ payload, ++ secret, ++ algorithm="HS256", ++ headers={"crit": "b64"}, ++ ) ++ ++ with pytest.raises(InvalidTokenError, match="must be a non-empty list"): ++ jws.decode(token, secret, algorithms=["HS256"]) ++ ++ def test_decode_rejects_crit_with_non_string_values( ++ self, jws: PyJWS, payload: bytes ++ ) -> None: ++ secret = "secret" ++ token = jws.encode( ++ payload, ++ secret, ++ algorithm="HS256", ++ headers={"crit": [123]}, ++ ) ++ ++ with pytest.raises(InvalidTokenError, match="values must be strings"): ++ jws.decode(token, secret, algorithms=["HS256"]) ++ ++ def test_decode_rejects_crit_extension_missing_from_header( ++ self, jws: PyJWS, payload: bytes ++ ) -> None: ++ secret = "secret" ++ token = jws.encode( ++ payload, ++ secret, ++ algorithm="HS256", ++ headers={"crit": ["b64"]}, ++ ) ++ ++ with pytest.raises(InvalidTokenError, match="missing from headers"): ++ jws.decode(token, secret, algorithms=["HS256"]) ++ ++ def test_decode_accepts_supported_crit_extension( ++ self, jws: PyJWS, payload: bytes ++ ) -> None: ++ secret = "secret" ++ token = jws.encode( ++ payload, ++ secret, ++ algorithm="HS256", ++ headers={"crit": ["b64"], "b64": False}, ++ is_payload_detached=True, ++ ) ++ ++ decoded = jws.decode( ++ token, ++ secret, ++ algorithms=["HS256"], ++ detached_payload=payload, ++ ) ++ assert decoded == payload ++ ++ def test_get_unverified_header_rejects_unknown_crit( ++ self, jws: PyJWS, payload: bytes ++ ) -> None: ++ secret = "secret" ++ token = jws.encode( ++ payload, ++ secret, ++ algorithm="HS256", ++ headers={"crit": ["x-unknown"], "x-unknown": "value"}, ++ ) ++ ++ with pytest.raises(InvalidTokenError, match="Unsupported critical extension"): ++ jws.get_unverified_header(token) +diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py +index d74973d..f884e83 100644 +--- a/tests/test_api_jwt.py ++++ b/tests/test_api_jwt.py +@@ -717,3 +717,21 @@ class TestJWT: + jwt.decode_complete(jwt_message, secret, algorithms=["HS256"], foo="bar") + assert len(record) == 1 + assert "foo" in str(record[0].message) ++ ++ # -------------------- Crit Header Tests -------------------- ++ ++ def test_decode_rejects_token_with_unknown_crit_extension(self, jwt: PyJWT) -> None: ++ """RFC 7515 §4.1.11: tokens with unsupported critical extensions MUST be rejected.""" ++ from jwt.exceptions import InvalidTokenError ++ ++ secret = "secret" ++ payload = {"sub": "attacker", "role": "admin"} ++ token = jwt.encode( ++ payload, ++ secret, ++ algorithm="HS256", ++ headers={"crit": ["x-custom-policy"], "x-custom-policy": "require-mfa"}, ++ ) ++ ++ with pytest.raises(InvalidTokenError, match="Unsupported critical extension"): ++ jwt.decode(token, secret, algorithms=["HS256"]) diff -Nru pyjwt-2.6.0/debian/patches/series pyjwt-2.6.0/debian/patches/series --- pyjwt-2.6.0/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 +++ pyjwt-2.6.0/debian/patches/series 2026-04-28 14:47:59.000000000 +0000 @@ -0,0 +1 @@ +0001-Merge-commit-from-fork.patch