diff mbox series

[meta-python,scarthgap] python3-pyjwt: Fix CVE-2026-32597

Message ID 20260318055546.95652-1-hprajapati@mvista.com
State New
Headers show
Series [meta-python,scarthgap] python3-pyjwt: Fix CVE-2026-32597 | expand

Commit Message

Hitendra Prajapati March 18, 2026, 5:55 a.m. UTC
Details: https://nvd.nist.gov/vuln/detail/CVE-2026-32597

Backport commit[1] which fixes this vulnerability as mentioned in [2].

[1] https://github.com/jpadilla/pyjwt/commit/051ea341b5573fe3edcd53042f347929b92c2b92
[2] https://security-tracker.debian.org/tracker/CVE-2026-32597

Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
---
 .../python/python3-pyjwt/CVE-2026-32597.patch | 216 ++++++++++++++++++
 .../python/python3-pyjwt_2.8.0.bb             |   1 +
 2 files changed, 217 insertions(+)
 create mode 100644 meta-python/recipes-devtools/python/python3-pyjwt/CVE-2026-32597.patch
diff mbox series

Patch

diff --git a/meta-python/recipes-devtools/python/python3-pyjwt/CVE-2026-32597.patch b/meta-python/recipes-devtools/python/python3-pyjwt/CVE-2026-32597.patch
new file mode 100644
index 0000000000..c38628d0b3
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-pyjwt/CVE-2026-32597.patch
@@ -0,0 +1,216 @@ 
+From 051ea341b5573fe3edcd53042f347929b92c2b92 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Jos=C3=A9=20Padilla?= <jpadilla@webapplicate.com>
+Date: Thu, 12 Mar 2026 12:46:08 -0400
+Subject: [PATCH] Merge commit from fork
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Co-authored-by: José Padilla <jpadilla@users.noreply.github.com>
+
+CVE: CVE-2026-32597
+Upstream-Status: Backport [https://github.com/jpadilla/pyjwt/commit/051ea341b5573fe3edcd53042f347929b92c2b92]
+Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
+---
+ CHANGELOG.rst         |  2 +
+ jwt/api_jws.py        | 27 +++++++++++++-
+ tests/test_api_jws.py | 87 +++++++++++++++++++++++++++++++++++++++++++
+ tests/test_api_jwt.py | 18 +++++++++
+ 4 files changed, 132 insertions(+), 2 deletions(-)
+
+diff --git a/CHANGELOG.rst b/CHANGELOG.rst
+index 8bc2319..289f45b 100644
+--- a/CHANGELOG.rst
++++ b/CHANGELOG.rst
+@@ -26,6 +26,8 @@ Changed
+ 
+ Fixed
+ ~~~~~
++- Validate the crit (Critical) Header Parameter defined in RFC 7515 §4.1.11. by @dmbs335 in
++  `GHSA-752w-5fwx-jx9f <https://github.com/jpadilla/pyjwt/security/advisories/GHSA-752w-5fwx-jx9f>`__
+ 
+ Added
+ ~~~~~
+diff --git a/jwt/api_jws.py b/jwt/api_jws.py
+index fa6708c..1750442 100644
+--- a/jwt/api_jws.py
++++ b/jwt/api_jws.py
+@@ -129,7 +129,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"]:
+@@ -197,6 +197,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(
+@@ -309,14 +311,35 @@ class PyJWS:
+         if not alg_obj.verify(signing_input, prepared_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: Any) -> 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 3385716..434874b 100644
+--- a/tests/test_api_jws.py
++++ b/tests/test_api_jws.py
+@@ -815,3 +815,90 @@ 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 82b9299..618b60f 100644
+--- a/tests/test_api_jwt.py
++++ b/tests/test_api_jwt.py
+@@ -802,3 +802,21 @@ class TestJWT:
+             options={"strict_aud": True},
+             algorithms=["HS256"],
+         )
++
++    # -------------------- 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"])
+-- 
+2.50.1
+
diff --git a/meta-python/recipes-devtools/python/python3-pyjwt_2.8.0.bb b/meta-python/recipes-devtools/python/python3-pyjwt_2.8.0.bb
index 92a8f44c6b..9753559171 100644
--- a/meta-python/recipes-devtools/python/python3-pyjwt_2.8.0.bb
+++ b/meta-python/recipes-devtools/python/python3-pyjwt_2.8.0.bb
@@ -5,6 +5,7 @@  HOMEPAGE = "http://github.com/jpadilla/pyjwt"
 LICENSE = "MIT"
 LIC_FILES_CHKSUM = "file://LICENSE;md5=e4b56d2c9973d8cf54655555be06e551"
 
+SRC_URI += "file://CVE-2026-32597.patch"
 SRC_URI[sha256sum] = "57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"
 
 PYPI_PACKAGE = "PyJWT"