Open
fix CVE-2026-48526 #51
lipengyu001Add on Jul 23
fix CVE-2026-48526 #51
Open
Total: 3 files changed+368-1
| @@ -0,0 +1,91 @@ | |||
| 1 | +From 95791b1759b8aa4f2203575d344d5c78564cdc81 Mon Sep 17 00:00:00 2001 | ||
| 2 | +From: =?UTF-8?q?Jos=C3=A9=20Padilla?= <jpadilla@users.noreply.github.com> | ||
| 3 | +Date: Thu, 21 May 2026 14:11:10 -0400 | ||
| 4 | +Subject: [PATCH] Reject JWK JSON documents used as raw HMAC secrets | ||
| 5 | +MIME-Version: 1.0 | ||
| 6 | +Content-Type: text/plain; charset=UTF-8 | ||
| 7 | +Content-Transfer-Encoding: 8bit | ||
| 8 | + | ||
| 9 | +Backport the CVE-2026-48526 part of upstream commit | ||
| 10 | +95791b1759b8aa4f2203575d344d5c78564cdc81 to PyJWT 2.8.0. | ||
| 11 | + | ||
| 12 | +HMACAlgorithm accepts arbitrary bytes as a secret. Reject JSON Web Key | ||
| 13 | +documents passed directly as HMAC secrets so that a public asymmetric JWK | ||
| 14 | +cannot be reused to forge an HS* token when an application allows mixed | ||
| 15 | +symmetric and asymmetric algorithms. | ||
| 16 | + | ||
| 17 | +The upstream release commit also contains fixes for other advisories and | ||
| 18 | +2.13.0-only hardening. Those unrelated changes are intentionally omitted. | ||
| 19 | + | ||
| 20 | +CVE: CVE-2026-48526 | ||
| 21 | +Upstream-commit: 95791b1759b8aa4f2203575d344d5c78564cdc81 | ||
| 22 | +Signed-off-by: José Padilla <jpadilla@users.noreply.github.com> | ||
| 23 | +--- | ||
| 24 | + jwt/algorithms.py | 16 ++++++++++++++++ | ||
| 25 | + tests/test_algorithms.py | 24 ++++++++++++++++++++++++ | ||
| 26 | + 2 files changed, 40 insertions(+) | ||
| 27 | + | ||
| 28 | +diff --git a/jwt/algorithms.py b/jwt/algorithms.py | ||
| 29 | +index 8f27040..77985ca 100644 | ||
| 30 | +--- a/jwt/algorithms.py | ||
| 31 | ++++ b/jwt/algorithms.py | ||
| 32 | +@@ -269,6 +269,22 @@ class HMACAlgorithm(Algorithm): | ||
| 33 | + " should not be used as an HMAC secret." | ||
| 34 | + ) | ||
| 35 | + | ||
| 36 | ++ # Defense against algorithm-confusion attacks: HMACAlgorithm accepts | ||
| 37 | ++ # arbitrary bytes as a secret, so reject JWK documents that should be | ||
| 38 | ++ # loaded with PyJWK / from_jwk instead of used as their JSON text. | ||
| 39 | ++ stripped = key_bytes.lstrip() | ||
| 40 | ++ if stripped.startswith(b"{"): | ||
| 41 | ++ try: | ||
| 42 | ++ jwk_obj = json.loads(key_bytes) | ||
| 43 | ++ except ValueError: | ||
| 44 | ++ jwk_obj = None | ||
| 45 | ++ if isinstance(jwk_obj, dict) and "kty" in jwk_obj: | ||
| 46 | ++ raise InvalidKeyError( | ||
| 47 | ++ "The specified key looks like a JWK and should not be " | ||
| 48 | ++ "used directly as an HMAC secret. Load it via " | ||
| 49 | ++ "PyJWK / HMACAlgorithm.from_jwk first." | ||
| 50 | ++ ) | ||
| 51 | ++ | ||
| 52 | + return key_bytes | ||
| 53 | + | ||
| 54 | + @overload | ||
| 55 | +diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py | ||
| 56 | +index 145a3c8..4561bc2 100644 | ||
| 57 | +--- a/tests/test_algorithms.py | ||
| 58 | ++++ b/tests/test_algorithms.py | ||
| 59 | +@@ -105,6 +105,30 @@ class TestAlgorithms: | ||
| 60 | + with pytest.raises(InvalidKeyError): | ||
| 61 | + algo.from_jwk(keyfile.read()) | ||
| 62 | + | ||
| 63 | ++ @pytest.mark.parametrize( | ||
| 64 | ++ "jwk_file", | ||
| 65 | ++ [ | ||
| 66 | ++ "jwk_rsa_pub.json", | ||
| 67 | ++ "jwk_ec_pub_P-256.json", | ||
| 68 | ++ "jwk_okp_pub_Ed25519.json", | ||
| 69 | ++ "jwk_hmac.json", | ||
| 70 | ++ ], | ||
| 71 | ++ ) | ||
| 72 | ++ def test_hmac_prepare_key_rejects_jwk_json(self, jwk_file): | ||
| 73 | ++ algo = HMACAlgorithm(HMACAlgorithm.SHA256) | ||
| 74 | ++ | ||
| 75 | ++ with open(key_path(jwk_file)) as keyfile: | ||
| 76 | ++ with pytest.raises(InvalidKeyError, match="looks like a JWK"): | ||
| 77 | ++ algo.prepare_key(keyfile.read()) | ||
| 78 | ++ | ||
| 79 | ++ def test_hmac_prepare_key_accepts_json_without_kty(self): | ||
| 80 | ++ # JSON without a kty member is an ordinary JSON-shaped secret, not a | ||
| 81 | ++ # JWK, and must remain compatible with earlier PyJWT releases. | ||
| 82 | ++ algo = HMACAlgorithm(HMACAlgorithm.SHA256) | ||
| 83 | ++ | ||
| 84 | ++ key = algo.prepare_key('{"this": "is just a json-shaped secret"}') | ||
| 85 | ++ assert key == b'{"this": "is just a json-shaped secret"}' | ||
| 86 | ++ | ||
| 87 | + @crypto_required | ||
| 88 | + def test_rsa_should_parse_pem_public_key(self): | ||
| 89 | + algo = RSAAlgorithm(RSAAlgorithm.SHA256) | ||
| 90 | +-- | ||
| 91 | +2.43.0 | ||
| @@ -0,0 +1,270 @@ | |||
| 1 | +From 95791b1759b8aa4f2203575d344d5c78564cdc81 Mon Sep 17 00:00:00 2001 | ||
| 2 | +From: José Padilla <jpadilla@users.noreply.github.com> | ||
| 3 | +Date: Thu, 21 May 2026 14:11:10 -0400 | ||
| 4 | +Subject: [PATCH] Backport remaining 2.13.0 security hardening | ||
| 5 | + | ||
| 6 | +Backport the applicable non-CVE-2026-48526 security fixes from upstream | ||
| 7 | +commit 95791b1759b8aa4f2203575d344d5c78564cdc81 to PyJWT 2.8.0. | ||
| 8 | + | ||
| 9 | +This patch: | ||
| 10 | + | ||
| 11 | +* rejects empty HMAC keys; | ||
| 12 | +* enforces RFC 7797 requirements for detached (b64=false) JWS payloads and | ||
| 13 | + avoids decoding a non-empty detached payload segment before verification; | ||
| 14 | +* restricts PyJWKClient endpoints to HTTP(S); and | ||
| 15 | +* preserves a previously cached JWKS when a refresh fails. | ||
| 16 | + | ||
| 17 | +The PyJWK algorithm-binding and per-call minimum-key-length fixes are not | ||
| 18 | +included: PyJWT 2.8.0 does not yet accept PyJWK instances in the verification | ||
| 19 | +path and does not implement enforce_minimum_key_length. | ||
| 20 | + | ||
| 21 | +Upstream-commit: 95791b1759b8aa4f2203575d344d5c78564cdc81 | ||
| 22 | +--- | ||
| 23 | + jwt/algorithms.py | 3 +++ | ||
| 24 | + jwt/api_jws.py | 28 +++++++++++++++++++++------- | ||
| 25 | + jwt/jwks_client.py | 22 +++++++++++++----- | ||
| 26 | + tests/test_algorithms.py | 7 +++++++ | ||
| 27 | + tests/test_api_jws.py | 20 +++++++++++++++----- | ||
| 28 | + tests/test_jwks_client.py | 10 ++++++++-- | ||
| 29 | + 6 files changed, 67 insertions(+), 23 deletions(-) | ||
| 30 | + | ||
| 31 | +diff --git a/jwt/algorithms.py b/jwt/algorithms.py | ||
| 32 | +index 7fbdfa8..d862e5b 100644 | ||
| 33 | +--- a/jwt/algorithms.py | ||
| 34 | ++++ b/jwt/algorithms.py | ||
| 35 | +@@ -264,6 +264,9 @@ class HMACAlgorithm(Algorithm): | ||
| 36 | + def prepare_key(self, key: str | bytes) -> bytes: | ||
| 37 | + key_bytes = force_bytes(key) | ||
| 38 | + | ||
| 39 | ++ if len(key_bytes) == 0: | ||
| 40 | ++ raise InvalidKeyError("HMAC key must not be empty.") | ||
| 41 | ++ | ||
| 42 | + if is_pem_format(key_bytes) or is_ssh_key(key_bytes): | ||
| 43 | + raise InvalidKeyError( | ||
| 44 | + "The specified key is an asymmetric key or x509 certificate and" | ||
| 45 | +diff --git a/jwt/api_jws.py b/jwt/api_jws.py | ||
| 46 | +index fa6708c..3a8f29c 100644 | ||
| 47 | +--- a/jwt/api_jws.py | ||
| 48 | ++++ b/jwt/api_jws.py | ||
| 49 | +@@ -137,6 +137,11 @@ class PyJWS: | ||
| 50 | + | ||
| 51 | + if is_payload_detached: | ||
| 52 | + header["b64"] = False | ||
| 53 | ++ existing_crit = header.get("crit", []) | ||
| 54 | ++ if not isinstance(existing_crit, list): | ||
| 55 | ++ raise InvalidTokenError("Invalid 'crit' header: must be a list") | ||
| 56 | ++ if "b64" not in existing_crit: | ||
| 57 | ++ header["crit"] = [*existing_crit, "b64"] | ||
| 58 | + elif "b64" in header: | ||
| 59 | + # True is the standard value for b64, so no need for it | ||
| 60 | + del header["b64"] | ||
| 61 | +@@ -200,6 +205,12 @@ class PyJWS: | ||
| 62 | + self._validate_headers(header) | ||
| 63 | + | ||
| 64 | + if header.get("b64", True) is False: | ||
| 65 | ++ crit = header.get("crit") or [] | ||
| 66 | ++ if not isinstance(crit, list) or "b64" not in crit: | ||
| 67 | ++ raise InvalidTokenError( | ||
| 68 | ++ "The 'b64' header parameter requires 'b64' to be " | ||
| 69 | ++ "listed in 'crit'." | ||
| 70 | ++ ) | ||
| 71 | + if detached_payload is None: | ||
| 72 | + raise DecodeError( | ||
| 73 | + 'It is required that you pass in a value for the "detached_payload" argument to decode a message having the b64 header set to false.' | ||
| 74 | +@@ -272,10 +283,15 @@ class PyJWS: | ||
| 75 | + if not isinstance(header, dict): | ||
| 76 | + raise DecodeError("Invalid header string: must be a json object") | ||
| 77 | + | ||
| 78 | +- try: | ||
| 79 | +- payload = base64url_decode(payload_segment) | ||
| 80 | +- except (TypeError, binascii.Error) as err: | ||
| 81 | +- raise DecodeError("Invalid payload padding") from err | ||
| 82 | ++ if header.get("b64", True) is False: | ||
| 83 | ++ if payload_segment: | ||
| 84 | ++ raise DecodeError("Payload segment must be empty when 'b64' is false.") | ||
| 85 | ++ payload = b"" | ||
| 86 | ++ else: | ||
| 87 | ++ try: | ||
| 88 | ++ payload = base64url_decode(payload_segment) | ||
| 89 | ++ except (TypeError, binascii.Error) as err: | ||
| 90 | ++ raise DecodeError("Invalid payload padding") from err | ||
| 91 | + | ||
| 92 | + try: | ||
| 93 | + signature = base64url_decode(crypto_segment) | ||
| 94 | +diff --git a/jwt/jwks_client.py b/jwt/jwks_client.py | ||
| 95 | +index f19b10a..be8cb4e 100644 | ||
| 96 | +--- a/jwt/jwks_client.py | ||
| 97 | ++++ b/jwt/jwks_client.py | ||
| 98 | +@@ -4,6 +4,7 @@ from functools import lru_cache | ||
| 99 | + from ssl import SSLContext | ||
| 100 | + from typing import Any, Dict, List, Optional | ||
| 101 | + from urllib.error import URLError | ||
| 102 | ++from urllib.parse import urlparse | ||
| 103 | + | ||
| 104 | + from .api_jwk import PyJWK, PyJWKSet | ||
| 105 | + from .api_jwt import decode_complete as decode_token | ||
| 106 | +@@ -25,6 +26,12 @@ class PyJWKClient: | ||
| 107 | + ): | ||
| 108 | + if headers is None: | ||
| 109 | + headers = {} | ||
| 110 | ++ scheme = urlparse(uri).scheme.lower() | ||
| 111 | ++ if scheme not in ("http", "https"): | ||
| 112 | ++ raise PyJWKClientError( | ||
| 113 | ++ f"Invalid JWKS URI scheme {scheme!r}: only 'http' and 'https' " | ||
| 114 | ++ f"are supported." | ||
| 115 | ++ ) | ||
| 116 | + self.uri = uri | ||
| 117 | + self.jwk_set_cache: Optional[JWKSetCache] = None | ||
| 118 | + self.headers = headers | ||
| 119 | +@@ -48,7 +55,6 @@ class PyJWKClient: | ||
| 120 | + self.get_signing_key = lru_cache(maxsize=max_cached_keys)(self.get_signing_key) # type: ignore | ||
| 121 | + | ||
| 122 | + def fetch_data(self) -> Any: | ||
| 123 | +- jwk_set: Any = None | ||
| 124 | + try: | ||
| 125 | + r = urllib.request.Request(url=self.uri, headers=self.headers) | ||
| 126 | + with urllib.request.urlopen( | ||
| 127 | +@@ -59,11 +65,9 @@ class PyJWKClient: | ||
| 128 | + raise PyJWKClientConnectionError( | ||
| 129 | + f'Fail to fetch data from the url, err: "{e}"' | ||
| 130 | + ) | ||
| 131 | +- else: | ||
| 132 | +- return jwk_set | ||
| 133 | +- finally: | ||
| 134 | +- if self.jwk_set_cache is not None: | ||
| 135 | +- self.jwk_set_cache.put(jwk_set) | ||
| 136 | ++ if self.jwk_set_cache is not None: | ||
| 137 | ++ self.jwk_set_cache.put(jwk_set) | ||
| 138 | ++ return jwk_set | ||
| 139 | + | ||
| 140 | + def get_jwk_set(self, refresh: bool = False) -> PyJWKSet: | ||
| 141 | + data = None | ||
| 142 | +diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py | ||
| 143 | +index 4dec48a..42e7ddc 100644 | ||
| 144 | +--- a/tests/test_algorithms.py | ||
| 145 | ++++ b/tests/test_algorithms.py | ||
| 146 | +@@ -132,6 +132,13 @@ class TestAlgorithms: | ||
| 147 | + key = algo.prepare_key('{"this": "is just a json-shaped secret"}') | ||
| 148 | + assert key == b'{"this": "is just a json-shaped secret"}' | ||
| 149 | + | ||
| 150 | ++ @pytest.mark.parametrize("empty_key", ["", b""]) | ||
| 151 | ++ def test_hmac_prepare_key_rejects_empty_key(self, empty_key): | ||
| 152 | ++ algo = HMACAlgorithm(HMACAlgorithm.SHA256) | ||
| 153 | ++ | ||
| 154 | ++ with pytest.raises(InvalidKeyError, match="must not be empty"): | ||
| 155 | ++ algo.prepare_key(empty_key) | ||
| 156 | ++ | ||
| 157 | + @crypto_required | ||
| 158 | + def test_rsa_should_parse_pem_public_key(self): | ||
| 159 | + algo = RSAAlgorithm(RSAAlgorithm.SHA256) | ||
| 160 | +diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py | ||
| 161 | +index 3385716..adbe546 100644 | ||
| 162 | +--- a/tests/test_api_jws.py | ||
| 163 | ++++ b/tests/test_api_jws.py | ||
| 164 | +@@ -7,6 +7,7 @@ from jwt.api_jws import PyJWS | ||
| 165 | + from jwt.exceptions import ( | ||
| 166 | + DecodeError, | ||
| 167 | + InvalidAlgorithmError, | ||
| 168 | ++ InvalidKeyError, | ||
| 169 | + InvalidSignatureError, | ||
| 170 | + InvalidTokenError, | ||
| 171 | + ) | ||
| 172 | +@@ -362,18 +363,16 @@ class TestJWS: | ||
| 173 | + right_secret = "foo" | ||
| 174 | + jws_message = jws.encode(payload, right_secret) | ||
| 175 | + | ||
| 176 | +- with pytest.raises(DecodeError): | ||
| 177 | ++ with pytest.raises(InvalidKeyError, match="must not be empty"): | ||
| 178 | + jws.decode(jws_message, algorithms=["HS256"]) | ||
| 179 | + | ||
| 180 | + def test_verify_signature_with_no_secret(self, jws, payload): | ||
| 181 | + right_secret = "foo" | ||
| 182 | + jws_message = jws.encode(payload, right_secret) | ||
| 183 | + | ||
| 184 | +- with pytest.raises(DecodeError) as exc: | ||
| 185 | ++ with pytest.raises(InvalidKeyError, match="must not be empty"): | ||
| 186 | + jws.decode(jws_message, algorithms=["HS256"]) | ||
| 187 | + | ||
| 188 | +- assert "Signature verification" in str(exc.value) | ||
| 189 | +- | ||
| 190 | + def test_verify_signature_with_no_algo_header_throws_exception(self, jws, payload): | ||
| 191 | + example_jws = b"e30.eyJhIjo1fQ.KEh186CjVw_Q8FadjJcaVnE7hO5Z9nHBbU8TgbhHcBY" | ||
| 192 | + | ||
| 193 | +@@ -752,6 +752,7 @@ class TestJWS: | ||
| 194 | + | ||
| 195 | + assert "b64" in msg_header_obj | ||
| 196 | + assert msg_header_obj["b64"] is False | ||
| 197 | ++ assert "b64" in msg_header_obj["crit"] | ||
| 198 | + # Check that the payload is not inside the token | ||
| 199 | + assert not msg_payload | ||
| 200 | + | ||
| 201 | +@@ -766,7 +767,7 @@ class TestJWS: | ||
| 202 | + assert "b64" not in msg_header_obj | ||
| 203 | + assert msg_payload | ||
| 204 | + | ||
| 205 | +- def test_decode_detached_content_without_proper_argument(self, jws): | ||
| 206 | ++ def test_decode_detached_content_requires_critical_b64_header(self, jws): | ||
| 207 | + example_jws = ( | ||
| 208 | + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2V9" | ||
| 209 | + "." | ||
| 210 | +@@ -774,14 +775,27 @@ class TestJWS: | ||
| 211 | + ) | ||
| 212 | + example_secret = "secret" | ||
| 213 | + | ||
| 214 | +- with pytest.raises(DecodeError) as exc: | ||
| 215 | ++ with pytest.raises(InvalidTokenError) as exc: | ||
| 216 | + jws.decode(example_jws, example_secret, algorithms=["HS256"]) | ||
| 217 | + | ||
| 218 | + assert ( | ||
| 219 | +- 'It is required that you pass in a value for the "detached_payload" argument to decode a message having the b64 header set to false.' | ||
| 220 | ++ "requires 'b64' to be listed in 'crit'" | ||
| 221 | + in str(exc.value) | ||
| 222 | + ) | ||
| 223 | + | ||
| 224 | ++ def test_decode_detached_content_rejects_nonempty_payload_segment(self, jws, payload): | ||
| 225 | ++ token = jws.encode(payload, "secret", algorithm="HS256", is_payload_detached=True) | ||
| 226 | ++ header, _, signature = token.split(".") | ||
| 227 | ++ malformed_token = f"{header}.Zm9v.{signature}" | ||
| 228 | ++ | ||
| 229 | ++ with pytest.raises(DecodeError, match="Payload segment must be empty"): | ||
| 230 | ++ jws.decode( | ||
| 231 | ++ malformed_token, | ||
| 232 | ++ "secret", | ||
| 233 | ++ algorithms=["HS256"], | ||
| 234 | ++ detached_payload=payload, | ||
| 235 | ++ ) | ||
| 236 | ++ | ||
| 237 | + def test_decode_warns_on_unsupported_kwarg(self, jws, payload): | ||
| 238 | + secret = "secret" | ||
| 239 | + jws_message = jws.encode( | ||
| 240 | +diff --git a/tests/test_jwks_client.py b/tests/test_jwks_client.py | ||
| 241 | +index c3951ea..d6da9ec 100644 | ||
| 242 | +--- a/tests/test_jwks_client.py | ||
| 243 | ++++ b/tests/test_jwks_client.py | ||
| 244 | +@@ -88,6 +88,11 @@ def mocked_timeout(): | ||
| 245 | + | ||
| 246 | + @crypto_required | ||
| 247 | + class TestPyJWKClient: | ||
| 248 | ++ @pytest.mark.parametrize("url", ["file:///etc/passwd", "ftp://example.test/jwks", "data:application/json,{}"]) | ||
| 249 | ++ def test_rejects_non_http_jwks_uri(self, url): | ||
| 250 | ++ with pytest.raises(PyJWKClientError, match="only 'http' and 'https'"): | ||
| 251 | ++ PyJWKClient(url) | ||
| 252 | ++ | ||
| 253 | + def test_fetch_data_forwards_headers_to_correct_url(self): | ||
| 254 | + url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" | ||
| 255 | + | ||
| 256 | +@@ -271,7 +276,7 @@ class TestPyJWKClient: | ||
| 257 | + | ||
| 258 | + assert repeated_call.call_count == 1 | ||
| 259 | + | ||
| 260 | +- def test_get_jwt_set_failed_request_should_clear_cache(self): | ||
| 261 | ++ def test_get_jwt_set_failed_request_preserves_cache(self): | ||
| 262 | + url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json" | ||
| 263 | + | ||
| 264 | + jwks_client = PyJWKClient(url) | ||
| 265 | +@@ -285 +290,2 @@ class TestPyJWKClient: | ||
| 266 | +- assert jwks_client.jwk_set_cache is None | ||
| 267 | ++ assert jwks_client.jwk_set_cache is not None | ||
| 268 | ++ assert jwks_client.jwk_set_cache.get() is not None | ||
| 269 | +-- | ||
| 270 | +2.43.0 | ||
| @@ -2,12 +2,14 @@ | |||
| 2 | %bcond_without tests | 2 | %bcond_without tests |
| 3 | Name: python-jwt | 3 | Name: python-jwt |
| 4 | Version: 2.8.0 | 4 | Version: 2.8.0 |
| 5 | -Release: 3 | 5 | +Release: 4 |
| 6 | Summary: JSON Web Token implementation in Python | 6 | Summary: JSON Web Token implementation in Python |
| 7 | License: MIT | 7 | License: MIT |
| 8 | URL: https://github.com/jpadilla/pyjwt | 8 | URL: https://github.com/jpadilla/pyjwt |
| 9 | Source0: https://files.pythonhosted.org/packages/source/P/PyJWT/PyJWT-%{version}.tar.gz | 9 | Source0: https://files.pythonhosted.org/packages/source/P/PyJWT/PyJWT-%{version}.tar.gz |
| 10 | Patch6000: backport-CVE-2026-32597.patch | 10 | Patch6000: backport-CVE-2026-32597.patch |
| 11 | +Patch6001: backport-CVE-2026-48526.patch | ||
| 12 | +Patch6002: backport-PyJWT-security-hardening.patch | ||
| 11 | BuildArch: noarch | 13 | BuildArch: noarch |
| 12 | 14 | ||
| 13 | %if %{with tests} | 15 | %if %{with tests} |
| @@ -55,6 +57,10 @@ rm -f tests/test_jwks_client.py | |||
| 55 | %doc README.rst | 57 | %doc README.rst |
| 56 | 58 | ||
| 57 | %changelog | 59 | %changelog |
| 60 | +* Thu Jul 23 2026 lipengyu <lipengyu@kylinos.cn> - 2.8.0-4 | ||
| 61 | +- Fix CVE-2026-48526 | ||
| 62 | +- Backport applicable PyJWT security hardening | ||
| 63 | + | ||
| 58 | * Fri Jul 17 2026 xiaoo_robot <xiaoo_robot@petalmail.com> - 2.8.0-3 | 64 | * Fri Jul 17 2026 xiaoo_robot <xiaoo_robot@petalmail.com> - 2.8.0-3 |
| 59 | - Type:CVE | 65 | - Type:CVE |
| 60 | - CVE:CVE-2026-32597 | 66 | - CVE:CVE-2026-32597 |