Open
fix CVE-2026-48526 #51
Open
lipengyu001Add on Jul 23
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 tests2%bcond_without tests
3Name: python-jwt3Name: python-jwt
4Version: 2.8.04Version: 2.8.0
5-Release: 35+Release: 4
6Summary: JSON Web Token implementation in Python6Summary: JSON Web Token implementation in Python
7License: MIT7License: MIT
8URL: https://github.com/jpadilla/pyjwt8URL: https://github.com/jpadilla/pyjwt
9Source0: https://files.pythonhosted.org/packages/source/P/PyJWT/PyJWT-%{version}.tar.gz9Source0: https://files.pythonhosted.org/packages/source/P/PyJWT/PyJWT-%{version}.tar.gz
10Patch6000: backport-CVE-2026-32597.patch10Patch6000: backport-CVE-2026-32597.patch
11+Patch6001: backport-CVE-2026-48526.patch
12+Patch6002: backport-PyJWT-security-hardening.patch
11BuildArch: noarch13BuildArch: 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.rst57%doc README.rst
56 58 
57%changelog59%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-364* Fri Jul 17 2026 xiaoo_robot <xiaoo_robot@petalmail.com> - 2.8.0-3
59- Type:CVE65- Type:CVE
60- CVE:CVE-2026-3259766- CVE:CVE-2026-32597