mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-09-05 05:20:19 +00:00
This patch applies the upstream 2.13.0 backport for CVE-2026-48525. The upstream fix commit is referenced in [1], and the public CVE advisory is referenced in [2]. [1] https://github.com/jpadilla/pyjwt/commit/95791b1759b8aa4f2203575d344d5c78564cdc81 [2] https://github.com/advisories/GHSA-w7vc-732c-9m39 Signed-off-by: Hetvi Thakar <hthakar@cisco.com> Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
147 lines
6.5 KiB
Diff
147 lines
6.5 KiB
Diff
From e6152fb27dd439bf4541aab9b26a93134c48135f Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jos=C3=A9=20Padilla?= <jpadilla@users.noreply.github.com>
|
|
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-48525
|
|
Upstream-Status: Backport [https://github.com/jpadilla/pyjwt/commit/95791b1759b8aa4f2203575d344d5c78564cdc81]
|
|
|
|
Backport Changes:
|
|
- Split out the b64=false payload-decoding DoS fix because the upstream
|
|
commit bundles multiple CVEs.
|
|
- Omitted CHANGELOG.rst because it conflicted and is release documentation.
|
|
- Carried the focused
|
|
`test_decode_b64_false_rejects_non_empty_payload_segment` regression test.
|
|
Omitted other tests/test_api_jws.py hunks for unrelated bundled fixes.
|
|
- 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 <hthakar@cisco.com>
|
|
---
|
|
jwt/api_jws.py | 22 ++++++++++++++++++----
|
|
tests/test_api_jws.py | 36 +++++++++++++++++++++++++++++++++++-
|
|
2 files changed, 53 insertions(+), 5 deletions(-)
|
|
|
|
diff --git a/jwt/api_jws.py b/jwt/api_jws.py
|
|
index 91d2ae3..5a91bce 100644
|
|
--- a/jwt/api_jws.py
|
|
+++ b/jwt/api_jws.py
|
|
@@ -317,10 +317,24 @@ class PyJWS:
|
|
if not isinstance(header, dict):
|
|
raise DecodeError("Invalid header string: must be a json object")
|
|
|
|
- try:
|
|
- payload = base64url_decode(payload_segment)
|
|
- except (TypeError, binascii.Error) as err:
|
|
- raise DecodeError("Invalid payload padding") from err
|
|
+ if header.get("b64", True) is False:
|
|
+ # Detached payload form (RFC 7515 Appendix F): the compact-form
|
|
+ # payload segment must be empty; the caller supplies the actual
|
|
+ # payload via the `detached_payload` argument in decode_complete.
|
|
+ # Skipping the base64 decode here removes an unauthenticated work
|
|
+ # amplifier — otherwise an attacker can inflate the unused
|
|
+ # segment to force CPU + memory cost before the signature is
|
|
+ # even checked.
|
|
+ if payload_segment:
|
|
+ raise DecodeError(
|
|
+ "Payload segment must be empty when 'b64' is false."
|
|
+ )
|
|
+ payload = b""
|
|
+ else:
|
|
+ try:
|
|
+ payload = base64url_decode(payload_segment)
|
|
+ except (TypeError, binascii.Error) as err:
|
|
+ raise DecodeError("Invalid payload padding") from err
|
|
|
|
try:
|
|
signature = base64url_decode(crypto_segment)
|
|
diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py
|
|
index 0715b9e..01de813 100644
|
|
--- a/tests/test_api_jws.py
|
|
+++ b/tests/test_api_jws.py
|
|
@@ -12,7 +12,7 @@ from jwt.exceptions import (
|
|
InvalidSignatureError,
|
|
InvalidTokenError,
|
|
)
|
|
-from jwt.utils import base64url_decode
|
|
+from jwt.utils import base64url_decode, base64url_encode
|
|
from jwt.warnings import RemovedInPyjwt3Warning
|
|
|
|
from .utils import crypto_required, key_path, no_crypto_required
|
|
@@ -984,6 +984,40 @@ class TestJWS:
|
|
assert "b64" not in msg_header_obj
|
|
assert msg_payload
|
|
|
|
+ def test_decode_b64_false_rejects_non_empty_payload_segment(
|
|
+ self, jws: PyJWS, payload: bytes
|
|
+ ) -> None:
|
|
+ # RFC 7515 Appendix F detached form: when b64=false, the compact-
|
|
+ # serialization payload segment must be empty. PyJWT must reject a
|
|
+ # non-empty middle segment without doing any base64-decoding work
|
|
+ # on it — that decode used to be the unauthenticated DoS amplifier.
|
|
+ secret = "secret"
|
|
+ import hmac as _hmac
|
|
+ import hashlib as _hashlib
|
|
+
|
|
+ header_obj = {
|
|
+ "typ": "JWT",
|
|
+ "alg": "HS256",
|
|
+ "b64": False,
|
|
+ "crit": ["b64"],
|
|
+ }
|
|
+ header_b64 = base64url_encode(
|
|
+ json.dumps(header_obj, separators=(",", ":")).encode()
|
|
+ )
|
|
+ # Stuff the middle segment with arbitrary attacker-controlled bytes.
|
|
+ # This should be rejected without being base64-decoded.
|
|
+ attacker_segment = b"A" * 1024
|
|
+ signing_input = b".".join([header_b64, payload])
|
|
+ sig = _hmac.new(secret.encode(), signing_input, _hashlib.sha256).digest()
|
|
+ token = b".".join(
|
|
+ [header_b64, attacker_segment, base64url_encode(sig)]
|
|
+ ).decode()
|
|
+
|
|
+ with pytest.raises(DecodeError, match="Payload segment must be empty"):
|
|
+ jws.decode(
|
|
+ token, secret, algorithms=["HS256"], detached_payload=payload
|
|
+ )
|
|
+
|
|
def test_decode_detached_content_without_proper_argument(self, jws: PyJWS) -> None:
|
|
example_jws = (
|
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImI2NCI6ZmFsc2V9"
|