CVE-2026-48524
PyJWT allows DoS via unlimited JWKS requests from an unverified JWT `kid`.
- CVSS 3.7
- CWE-460
- Resource Management
- Remote
PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, 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. 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. This vulnerability is fixed in 2.13.0.
- CWE
- CWE-460
- CVSS base score
- 3.7
- Published
- 2026-05-28
- OWASP
- A10 Server-Side Request Forgery
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Resource Management
- Subcategory
- Server-Side Request Forgery (SSRF)
- Accessibility scope
- Remote
- Impact
- Denial of Service (DoS)
- Affected component
- PyJWT
- Fixed by upgrading
- Yes
Solution
Upgrade PyJWT to version 2.13.0 or later.
Vulnerable code sample
import jwt
from jwt import PyJWKClient
import threading
import http.server
import socketserver
import time
import uuid
# This mock server will act as the JWKS endpoint.
# It prints a message every time it receives a request.
class JWKSRequestHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
print("--> JWKS endpoint received a new HTTP request.")
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
# Respond with an empty set of keys to ensure the 'kid' is always "unknown".
self.wfile.write(b'{"keys":[]}')
# Suppress default server logging for a cleaner output.
def log_message(self, format, *args):
return
def run_jwks_server(port=8080):
with socketserver.TCPServer(("", port), JWKSRequestHandler) as httpd:
httpd.serve_forever()
# Start the mock JWKS server in a background thread.
jwks_server_thread = threading.Thread(target=run_jwks_server, daemon=True)
jwks_server_thread.start()
time.sleep(0.5) # Give the server a moment to start up.
# In an application, this client would be used to validate incoming JWTs.
# The URL points to our mock server.
jwks_uri = "http://localhost:8080/.well-known/jwks.json"
jwk_client = PyJWKClient(jwks_uri)
# Simulate an attacker sending multiple tokens, each with a new, random 'kid'.
# A real attack would involve sending these tokens to a web endpoint.
print("Simulating 5 validation attempts with unique, unknown 'kid' values...")
for i in range(5):
# The attacker controls the 'kid' in the unverified token header.
malicious_kid = str(uuid.uuid4())
try:
# This is the vulnerable operation. For each 'kid' not in its cache,
# the client makes a new HTTP request to the jwks_uri with no rate limiting.
print(f"Attempting to get key for kid: {malicious_kid[:12]}...")
signing_key = jwk_client.get_signing_key(malicious_kid)
except jwt.exceptions.PyJWKClientError:
# An error is expected because the key is not found.
# The vulnerability is the repeated, uncached network request itself.
passPatched code sample
# This code demonstrates the patched behavior of PyJWT (version 2.13.0 and later,
# though the specific fix for this type of issue was introduced earlier).
# The PyJWKClient now caches the JWKS response, preventing repeated requests for
# different unknown 'kid' values.
import http.server
import json
import socketserver
import threading
import time
# PyJWT is required. Install it with: pip install "PyJWT[crypto]"
from jwt import PyJWKClient
from jwt.exceptions import PyJWKClientError
# --- 1. Set up a mock JWKS endpoint ---
# This server will log every time it receives a request, allowing us to see
# how many times the PyJWKClient actually hits the network.
PORT = 8000
JWKS_URI = f"http://localhost:{PORT}/.well-known/jwks.json"
# A valid JWKS with one key. The demonstration will use kids that are NOT this one.
JWKS_CONTENT = {
"keys": [
{
"kty": "RSA",
"kid": "valid-key-id",
"use": "sig",
"n": "...",
"e": "AQAB",
}
]
}
class JwksHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
# This print statement is the core of the demonstration.
# It will only appear when a real HTTP request is made.
print(f"--- JWKS endpoint was requested by the client at {time.time():.2f} ---")
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(JWKS_CONTENT).encode("utf-8"))
def run_server():
with socketserver.TCPServer(("", PORT), JwksHttpRequestHandler) as httpd:
# print(f"Mock JWKS server running at port {PORT}")
httpd.serve_forever()
# Run the server in a background thread
server_thread = threading.Thread(target=run_server)
server_thread.daemon = True
server_thread.start()
time.sleep(0.5) # Give the server a moment to start up
# --- 2. Demonstrate the fixed (caching) behavior ---
# Initialize the client with a short cache lifespan for demonstration purposes.
# The client will cache the JWKS content for 5 seconds.
jwk_client = PyJWKClient(JWKS_URI, lifespan=5)
print("Attempting to get 3 different unknown keys in quick succession...")
for i in range(3):
unknown_kid = f"attacker-kid-{i}"
try:
print(f"Client: Requesting key with kid: '{unknown_kid}'")
jwk_client.get_signing_key(kid=unknown_kid)
except PyJWKClientError as e:
print(f"Client: Correctly failed to find key '{unknown_kid}'.")
# In the vulnerable version, the server would have received 3 requests.
# In the fixed version, it receives only one request because the result
# (the full set of keys) is cached.
print("\nWaiting for cache to expire (lifespan is 5 seconds)...")
time.sleep(6)
print("\nAttempting to get another unknown key after cache expiry...")
try:
print("Client: Requesting key with kid: 'another-attacker-kid'")
jwk_client.get_signing_key(kid="another-attacker-kid")
except PyJWKClientError as e:
print("Client: Correctly failed to find key 'another-attacker-kid'.")
# Because the cache expired, the client makes a second network request to
# refresh its cache before failing to find the key.
print("\nDemonstration complete. The server was only requested twice.")
# To stop the script, we would normally shut down the server, but since
# it's a daemon thread, it will exit when the main script finishes.Payload
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im5vbi1leGlzdGVudC1rZXktMTIzNDUifQ.eyJkYXRhIjoiYW55In0.
Cite this entry
@misc{vaitp:cve202648524,
title = {{PyJWT allows DoS via unlimited JWKS requests from an unverified JWT `kid`.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-48524},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-48524/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
