From 6590add3a8d18098b107fe446ff256ae92e7238f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Thu, 21 May 2026 14:11:10 -0400 Subject: [PATCH] Bundle security fixes and hardening into 2.13.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security: - `HMACAlgorithm.prepare_key` rejects JWK JSON documents passed as raw HMAC secrets to close an algorithm-confusion gap not covered by the existing PEM/SSH guard. Reported by @aradona91 in GHSA-xgmm-8j9v-c9wx. - Bind the JWT header `alg` to `PyJWK.algorithm_name` during verification so the caller's `algorithms` allow-list cannot be bypassed when decoding with a `PyJWK` / `PyJWKClient` key. Reported by @sushi-gif in GHSA-jq35-7prp-9v3f. - Skip the unconditional base64 decode of the compact-form payload segment when `b64=false` is set, and require that segment to be empty (RFC 7515 Appendix F detached form). Closes an unauthenticated DoS amplifier. Reported by @thesmartshadow in GHSA-w7vc-732c-9m39. - `PyJWKClient` rejects any URI whose scheme is not `http` or `https` so attacker-influenced URIs cannot read local files or reach unintended schemes via urllib's default `file://` / `ftp://` / `data:` handlers. Reported by @KEIJOT in GHSA-993g-76c3-p5m4. - Preserve the cached JWK Set on fetch errors in `PyJWKClient.fetch_data`. The previous `finally`-block `put(None)` pattern cleared the cache on any transient outage. Reported by @eddieran in GHSA-fhv5-28vv-h8m8. Fixes: - Reject empty HMAC keys outright in `HMACAlgorithm.prepare_key` with `InvalidKeyError` instead of accepting them with only a warning. Hardening prompted by reports from @SnailSploit and @spartan8806. - Forward per-call `options` (including `enforce_minimum_key_length`) from `PyJWT.decode` through to `PyJWS._verify_signature`. Thanks to @WLUB. - RFC 7797 §3 compliance for `b64=false`: encoder auto-adds `"b64"` to `crit`; decoder rejects tokens that set `b64=false` without listing it in `crit`. Thanks to @MachineLearning-Nerd. CVE: CVE-2026-48523 Upstream-Status: Backport [https://github.com/jpadilla/pyjwt/commit/95791b1759b8aa4f2203575d344d5c78564cdc81] Backport Changes: - Split out the PyJWK algorithm-binding validation because the upstream commit bundles multiple CVEs. - Omitted CHANGELOG.rst because it conflicted and is release documentation. - Omitted the 2.13.0 version bump and other bundled fixes; applicable CVE fixes are carried in separate patches. (cherry picked from commit 95791b1759b8aa4f2203575d344d5c78564cdc81) Signed-off-by: Hetvi Thakar --- jwt/api_jws.py | 10 ++++++++++ tests/test_api_jws.py | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/jwt/api_jws.py b/jwt/api_jws.py index 0ab7e4b..91d2ae3 100644 --- a/jwt/api_jws.py +++ b/jwt/api_jws.py @@ -348,6 +348,16 @@ class PyJWS: raise InvalidAlgorithmError("The specified alg value is not allowed") if isinstance(key, PyJWK): + # The PyJWK has a fixed algorithm bound at construction time. + # Verification must use that algorithm, not whatever the token + # header advertises, otherwise the caller's allow-list check + # above degenerates into a string compare with no behavioural + # effect on which algorithm actually verifies the signature. + if alg != key.algorithm_name: + raise InvalidAlgorithmError( + f"Token algorithm {alg!r} does not match the key's " + f"algorithm {key.algorithm_name!r}" + ) alg_obj = key.Algorithm prepared_key = key.key else: diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py index 9f7edc0..0715b9e 100644 --- a/tests/test_api_jws.py +++ b/tests/test_api_jws.py @@ -397,6 +397,33 @@ class TestJWS: with pytest.raises(InvalidAlgorithmError): jws.decode(example_jws, jwk) + def test_decodes_with_jwk_rejects_header_alg_outside_jwk_alg( + self, jws: PyJWS + ) -> None: + # Token header says HS256 and the caller's allow-list also accepts + # HS256, but the PyJWK is bound to HS512. Even though the allow-list + # would pass, verification must be locked to the PyJWK's algorithm + # rather than the header's — otherwise an attacker who controls a + # registered key can advertise a disallowed algorithm in the header + # and have it accepted. + jwk = PyJWK( + { + "kty": "oct", + "alg": "HS512", + "k": "c2VjcmV0", # "secret" + } + ) + example_jws = ( + b"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." + b"aGVsbG8gd29ybGQ." + b"gEW0pdU4kxPthjtehYdhxB9mMOGajt1xCKlGGXDJ8PM" + ) + + with pytest.raises( + InvalidAlgorithmError, match="does not match the key's algorithm" + ): + jws.decode(example_jws, jwk, algorithms=["HS256", "HS512"]) + # 'Control' Elliptic Curve jws created by another library. # Used to test for regressions that could affect both # encoding / decoding operations equally (causing tests