Open
reject unsupported b64 crit and fix CVE-2026-48526 #50
lipengyu001Add on Jul 21
reject unsupported b64 crit and fix CVE-2026-48526 #50
Open
Total: 3 files changed+207-2
| @@ -0,0 +1,93 @@ | |||
| 1 | +From 8fd2db6f6f2f5a26a71c3f5a3d2c6b57d801a7ce Mon Sep 17 00:00:00 2001 | ||
| 2 | +From: Codex <codex@localhost> | ||
| 3 | +Date: Tue, 21 Jul 2026 00:00:00 +0800 | ||
| 4 | +Subject: [PATCH] Reject b64 critical header on PyJWT 1.7.1 | ||
| 5 | + | ||
| 6 | +PyJWT 1.7.1 does not implement RFC 7797 b64=false / detached payload | ||
| 7 | +handling, so it must not advertise b64 as a supported critical header | ||
| 8 | +extension. Treat all crit entries as unsupported in this backport. | ||
| 9 | +--- | ||
| 10 | + jwt/api_jws.py | 5 +++-- | ||
| 11 | + tests/test_api_jws.py | 28 ++++++++++++++++++++++++++++ | ||
| 12 | + tests/test_api_jwt.py | 17 +++++++++++++++++ | ||
| 13 | + 3 files changed, 48 insertions(+), 2 deletions(-) | ||
| 14 | + | ||
| 15 | +diff --git a/jwt/api_jws.py b/jwt/api_jws.py | ||
| 16 | +index 876ada8..04d855b 100644 | ||
| 17 | +--- a/jwt/api_jws.py | ||
| 18 | ++++ b/jwt/api_jws.py | ||
| 19 | +@@ -21,8 +21,9 @@ from .utils import base64url_decode, base64url_encode, force_bytes, merge_dict | ||
| 20 | + class PyJWS(object): | ||
| 21 | + header_typ = 'JWT' | ||
| 22 | + | ||
| 23 | +- # Extensions that PyJWT actually understands and supports | ||
| 24 | +- _supported_crit = set(['b64']) | ||
| 25 | ++ # Extensions that PyJWT actually understands and supports. | ||
| 26 | ++ # PyJWT 1.7.1 does not implement RFC 7797 b64=false handling. | ||
| 27 | ++ _supported_crit = set() | ||
| 28 | + | ||
| 29 | + def __init__(self, algorithms=None, options=None): | ||
| 30 | + self._algorithms = get_default_algorithms() | ||
| 31 | +diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py | ||
| 32 | +index 26899b2..c76574d 100644 | ||
| 33 | +--- a/tests/test_api_jws.py | ||
| 34 | ++++ b/tests/test_api_jws.py | ||
| 35 | +@@ -679,3 +679,31 @@ class TestJWS: | ||
| 36 | + jws.encode(payload, 'secret', headers={'kid': None}) | ||
| 37 | + | ||
| 38 | + assert 'Key ID header parameter must be a string' == str(exc.value) | ||
| 39 | ++ | ||
| 40 | ++ def test_decode_rejects_b64_crit_extension(self, jws, payload): | ||
| 41 | ++ secret = 'secret' | ||
| 42 | ++ token = jws.encode( | ||
| 43 | ++ payload, | ||
| 44 | ++ secret, | ||
| 45 | ++ algorithm='HS256', | ||
| 46 | ++ headers={'crit': ['b64'], 'b64': False} | ||
| 47 | ++ ) | ||
| 48 | ++ | ||
| 49 | ++ with pytest.raises(InvalidTokenError) as exc: | ||
| 50 | ++ jws.decode(token, secret, algorithms=['HS256']) | ||
| 51 | ++ | ||
| 52 | ++ assert 'Unsupported critical extension: b64' == str(exc.value) | ||
| 53 | ++ | ||
| 54 | ++ def test_get_unverified_header_rejects_b64_crit_extension(self, jws, payload): | ||
| 55 | ++ secret = 'secret' | ||
| 56 | ++ token = jws.encode( | ||
| 57 | ++ payload, | ||
| 58 | ++ secret, | ||
| 59 | ++ algorithm='HS256', | ||
| 60 | ++ headers={'crit': ['b64'], 'b64': False} | ||
| 61 | ++ ) | ||
| 62 | ++ | ||
| 63 | ++ with pytest.raises(InvalidTokenError) as exc: | ||
| 64 | ++ jws.get_unverified_header(token) | ||
| 65 | ++ | ||
| 66 | ++ assert 'Unsupported critical extension: b64' == str(exc.value) | ||
| 67 | +diff --git a/tests/test_api_jwt.py b/tests/test_api_jwt.py | ||
| 68 | +index 01ca656..67cbdaa 100644 | ||
| 69 | +--- a/tests/test_api_jwt.py | ||
| 70 | ++++ b/tests/test_api_jwt.py | ||
| 71 | +@@ -520,3 +520,20 @@ class TestJWT: | ||
| 72 | + pass | ||
| 73 | + else: | ||
| 74 | + assert False, "Unexpected DeprecationWarning raised." | ||
| 75 | ++ | ||
| 76 | ++ def test_decode_rejects_b64_crit_extension(self, jwt): | ||
| 77 | ++ from jwt.exceptions import InvalidTokenError | ||
| 78 | ++ | ||
| 79 | ++ secret = 'secret' | ||
| 80 | ++ payload = {'sub': 'attacker', 'role': 'admin'} | ||
| 81 | ++ token = jwt.encode( | ||
| 82 | ++ payload, | ||
| 83 | ++ secret, | ||
| 84 | ++ algorithm='HS256', | ||
| 85 | ++ headers={'crit': ['b64'], 'b64': False} | ||
| 86 | ++ ) | ||
| 87 | ++ | ||
| 88 | ++ with pytest.raises(InvalidTokenError) as exc: | ||
| 89 | ++ jwt.decode(token, secret, algorithms=['HS256']) | ||
| 90 | ++ | ||
| 91 | ++ assert 'Unsupported critical extension: b64' == str(exc.value) | ||
| 92 | +-- | ||
| 93 | +1.8.3.1 | ||
| @@ -0,0 +1,103 @@ | |||
| 1 | +From 95791b1759b8aa4f2203575d344d5c78564cdc81 Mon Sep 17 00:00:00 2001 | ||
| 2 | +From: =?UTF-8?q?Jos=C3=A9=20Padilla?= <jpadilla@webapplicate.com> | ||
| 3 | +Date: Thu, 21 May 2026 12:00:00 -0400 | ||
| 4 | +Subject: [PATCH] Backport CVE-2026-48526 fix to PyJWT 1.7.1 | ||
| 5 | +MIME-Version: 1.0 | ||
| 6 | +Content-Type: text/plain; charset=UTF-8 | ||
| 7 | +Content-Transfer-Encoding: 8bit | ||
| 8 | + | ||
| 9 | +Reject JWK JSON documents passed as raw HMAC secrets in | ||
| 10 | +HMACAlgorithm.prepare_key to close an algorithm-confusion gap that the | ||
| 11 | +existing PEM/SSH guard does not cover. | ||
| 12 | + | ||
| 13 | +Adapted for PyJWT 1.7.1. The upstream fix is part of the 2.13.0 security | ||
| 14 | +bundle; only the CVE-2026-48526 HMAC/JWK guard is backported here. | ||
| 15 | +--- | ||
| 16 | + jwt/algorithms.py | 22 ++++++++++++++++++++++ | ||
| 17 | + tests/test_advisory.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ | ||
| 18 | + 2 files changed, 67 insertions(+), 1 deletion(-) | ||
| 19 | + | ||
| 20 | +diff --git a/jwt/algorithms.py b/jwt/algorithms.py | ||
| 21 | +index 8e4d739..3c39efb 100644 | ||
| 22 | +--- a/jwt/algorithms.py | ||
| 23 | ++++ b/jwt/algorithms.py | ||
| 24 | +@@ -147,1 +147,22 @@ class HMACAlgorithm(Algorithm): | ||
| 25 | +- return key | ||
| 26 | ++ # Defense against algorithm-confusion attacks: an attacker with | ||
| 27 | ++ # control over the token header can force this code path by setting | ||
| 28 | ++ # alg=HS*, and HMACAlgorithm is the only algorithm that accepts | ||
| 29 | ++ # arbitrary bytes as a valid secret. Other algorithms reject | ||
| 30 | ++ # non-key-shaped input naturally. Even a symmetric (kty=oct) JWK | ||
| 31 | ++ # should be loaded via HMACAlgorithm.from_jwk rather than fed as raw | ||
| 32 | ++ # JSON bytes (whose contents are not the secret material). | ||
| 33 | ++ stripped = key.lstrip() | ||
| 34 | ++ if stripped.startswith(b'{'): | ||
| 35 | ++ try: | ||
| 36 | ++ jwk_obj = json.loads(key) | ||
| 37 | ++ except ValueError: | ||
| 38 | ++ jwk_obj = None | ||
| 39 | ++ | ||
| 40 | ++ if isinstance(jwk_obj, dict) and 'kty' in jwk_obj: | ||
| 41 | ++ raise InvalidKeyError( | ||
| 42 | ++ 'The specified key looks like a JWK and should not be ' | ||
| 43 | ++ 'used directly as an HMAC secret. Load it via ' | ||
| 44 | ++ 'HMACAlgorithm.from_jwk first.' | ||
| 45 | ++ ) | ||
| 46 | ++ | ||
| 47 | ++ return key | ||
| 48 | +diff --git a/tests/test_advisory.py b/tests/test_advisory.py | ||
| 49 | +index e8065d4..b261430 100644 | ||
| 50 | +--- a/tests/test_advisory.py | ||
| 51 | ++++ b/tests/test_advisory.py | ||
| 52 | +@@ -107,3 +107,48 @@ class TestAdvisory: | ||
| 53 | + ssh_key_bytes, | ||
| 54 | + algorithms=jwt.algorithms.get_default_algorithms() | ||
| 55 | + ) | ||
| 56 | ++ | ||
| 57 | ++ def test_ghsa_xgmm_8j9v_c9wx(self): | ||
| 58 | ++ import hashlib | ||
| 59 | ++ import hmac | ||
| 60 | ++ import json | ||
| 61 | ++ from jwt.utils import base64url_encode | ||
| 62 | ++ | ||
| 63 | ++ # CVE-2026-48526: public-key JWK accepted as HMAC secret could allow | ||
| 64 | ++ # forged HS256 tokens when callers mix symmetric and asymmetric | ||
| 65 | ++ # algorithms and pass raw JSON JWK text as the verification key. | ||
| 66 | ++ rsa_jwk_json = json.dumps({ | ||
| 67 | ++ 'kty': 'RSA', | ||
| 68 | ++ 'n': 'AQAB', | ||
| 69 | ++ 'e': 'AQAB' | ||
| 70 | ++ }) | ||
| 71 | ++ | ||
| 72 | ++ signing_input = ( | ||
| 73 | ++ b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.' | ||
| 74 | ++ b'eyJzdWIiOiJhbGljZSIsImFkbWluIjp0cnVlfQ' | ||
| 75 | ++ ) | ||
| 76 | ++ signature = hmac.new( | ||
| 77 | ++ rsa_jwk_json.encode('utf-8'), | ||
| 78 | ++ signing_input, | ||
| 79 | ++ hashlib.sha256 | ||
| 80 | ++ ).digest() | ||
| 81 | ++ encoded_bad = b'.'.join(( | ||
| 82 | ++ signing_input, | ||
| 83 | ++ base64url_encode(signature) | ||
| 84 | ++ )) | ||
| 85 | ++ | ||
| 86 | ++ with pytest.raises(InvalidKeyError): | ||
| 87 | ++ jwt.decode( | ||
| 88 | ++ encoded_bad, | ||
| 89 | ++ rsa_jwk_json, | ||
| 90 | ++ algorithms=jwt.algorithms.get_default_algorithms() | ||
| 91 | ++ ) | ||
| 92 | ++ | ||
| 93 | ++ def test_hmac_should_reject_symmetric_jwk_as_raw_secret(self): | ||
| 94 | ++ oct_jwk_json = json.dumps({'kty': 'oct', 'k': 'c2VjcmV0'}) | ||
| 95 | ++ | ||
| 96 | ++ with pytest.raises(InvalidKeyError): | ||
| 97 | ++ jwt.decode( | ||
| 98 | ++ jwt.encode({'test': 1234}, oct_jwk_json, algorithm='HS256'), | ||
| 99 | ++ oct_jwk_json, | ||
| 100 | ++ algorithms=['HS256'] | ||
| 101 | ++ ) | ||
| 102 | +-- | ||
| 103 | +1.8.3.1 | ||
| @@ -2,7 +2,7 @@ | |||
| 2 | %bcond_with tests | 2 | %bcond_with tests |
| 3 | Name: python-jwt | 3 | Name: python-jwt |
| 4 | Version: 1.7.1 | 4 | Version: 1.7.1 |
| 5 | -Release: 5 | 5 | +Release: 6 |
| 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 |
| @@ -10,7 +10,9 @@ Source0: https://files.pythonhosted.org/packages/source/P/PyJWT/PyJWT | |||
| 10 | BuildArch: noarch | 10 | BuildArch: noarch |
| 11 | 11 | ||
| 12 | Patch6000: backport-CVE-2022-29217.patch | 12 | Patch6000: backport-CVE-2022-29217.patch |
| 13 | -Patch6001: backport-CVE-2026-32597-1.patch | 13 | +Patch6001: backport-CVE-2026-32597-1.patch |
| 14 | +Patch6002: backport-CVE-2026-32597-2.patch | ||
| 15 | +Patch6003: backport-CVE-2026-48526.patch | ||
| 14 | 16 | ||
| 15 | BuildRequires: python2-devel python2-setuptools python2-cryptography | 17 | BuildRequires: python2-devel python2-setuptools python2-cryptography |
| 16 | %if %{with tests} | 18 | %if %{with tests} |
| @@ -72,6 +74,13 @@ Requires: python%{python3_pkgversion}-cryptography | |||
| 72 | %doc README.rst | 74 | %doc README.rst |
| 73 | 75 | ||
| 74 | %changelog | 76 | %changelog |
| 77 | +* Tue Jul 21 2026 lipengyu <lipengyu@kylinos> - 1.7.1-6 | ||
| 78 | +- Type:CVE | ||
| 79 | +- CVE:CVE-2026-32597 CVE-2026-48526 | ||
| 80 | +- SUG:NA | ||
| 81 | +- DESC:reject unsupported b64 crit and fix CVE-2026-48526 | ||
| 82 | + | ||
| 83 | + | ||
| 75 | * Fri Jul 17 2026 xiaoo_robot <xiaoo_robot@petalmail.com> - 1.7.1-5 | 84 | * Fri Jul 17 2026 xiaoo_robot <xiaoo_robot@petalmail.com> - 1.7.1-5 |
| 76 | - Type:CVE | 85 | - Type:CVE |
| 77 | - CVE:CVE-2026-32597 | 86 | - CVE:CVE-2026-32597 |