Jun 15, 2026·8 min read·18 visits
Unauthenticated attackers can send JWTs with randomized KIDs to force connection errors on the target's upstream JWKS endpoint. A flaw in the error cleanup sequence then writes None to the cache, evicting all legitimate signing keys and preventing legitimate users from authenticating.
A logic flaw in PyJWT's PyJWKClient class allows remote unauthenticated attackers to trigger a complete authentication outage. By transmitting a volume of JWTs containing randomized, non-existent Key ID (kid) values, attackers force synchronous outbound JWKS resolution queries. When these queries fail or time out, a defect in the error cleanup code overwrites the local cache of valid signing keys with None, causing a denial of service.
The pyjwt library provides a widely adopted interface for encoding and decoding JSON Web Tokens (JWT) in Python. When deploying applications with asymmetric cryptography (such as RS256, ES256, or PS256), the verifying party requires access to the corresponding public key. The standard mechanism for dynamic key distribution is a JSON Web Key Set (JWKS), typically served over HTTPS by an Identity Provider (IdP) such as Auth0, Keycloak, or Okta. To optimize performance and reduce latency, the PyJWKClient class includes an in-memory caching mechanism (JWKSetCache) that stores retrieved keys, mitigating the need to query the remote JWKS endpoint on every token validation attempt.
A critical design choice in many JWT libraries, including legacy versions of pyjwt, is the parsing of the JWT header prior to cryptographic signature verification. The header contains metadata such as the Key ID (kid), which indicates which specific key from the JWK Set should be used to verify the signature. Because the kid is extracted from the unverified header, an attacker can manipulate this value arbitrarily. This exposes a significant attack surface: any incoming request containing a syntactically valid JWT, regardless of its signature validity, is processed by the verification pipeline up to the point of key retrieval.
CVE-2026-48524 describes a logical vulnerability in the handling of network errors during the retrieval of JWK Sets. When an unauthenticated remote attacker floods the target application with JWTs containing randomized, non-existent kid headers, the application attempts to resolve these keys by querying the remote JWKS endpoint. If these synchronous outbound queries fail—either due to rate-limiting by the IdP or socket timeouts—the cache-clearing logic in PyJWKClient is triggered. This behavior transforms a transient network error or rate-limiting event into a complete, application-wide authentication outage.
The root cause of CVE-2026-48524 lies in the exception-handling structure of the fetch_data method inside jwt/jwks_client.py. In Python, the finally block in a try-except-else-finally statement is guaranteed to execute regardless of whether an exception is raised, caught, or propagated. Prior to version 2.13.0, PyJWKClient.fetch_data utilized this construct to populate its local cache. The local variable jwk_set was initialized to None at the entry of the method.
During normal operations, a successful HTTP request populates jwk_set with the parsed dictionary containing the keys, which is then written to the cache via self.jwk_set_cache.put(jwk_set). However, if the outbound connection fails—due to connection reset, DNS failure, HTTP 429 rate limits, or read timeout—the interpreter raises an exception inside the try block. This exception is caught in the except block, which raises a PyJWKClientConnectionError. Before the exception propagates up the call stack to the caller, control is transferred to the finally block.
At this execution state, the jwk_set variable remains bound to its initial value of None. The conditional check if self.jwk_set_cache is not None: evaluates to true. Consequently, the interpreter executes self.jwk_set_cache.put(None). The cache backend implements a simple key-value replacement strategy. Writing None to the cache overwrites and effectively evicts the previously stored, valid JWK Set. This represents an instance of CWE-460 (Improper Cleanup on Thrown Exception) working in conjunction with CWE-755 (Improper Handling of Exceptional Conditions).
To understand the mechanics of the vulnerability and its remediation, we examine the logical transition between the vulnerable implementation and the patch introduced in version 2.13.0. The fundamental flaw was the placement of state-altering cache writes within the non-conditional cleanup path of the finally block.
# VULNERABLE CODE PATH (jwt/jwks_client.py)
def fetch_data(self) -> Any:
jwk_set: Any = None
try:
# Outbound network IO is initiated here
r = urllib.request.Request(url=self.uri, headers=self.headers)
with urllib.request.urlopen(
r, timeout=self.timeout, context=self.ssl_context
) as response:
jwk_set = json.loads(response.read().decode("utf-8"))
except Exception as e:
# Any network failure or timeout redirects here
raise PyJWKClientConnectionError(
f'Fail to fetch data from the url, err: "{e}"'
) from e
else:
return jwk_set
finally:
# EXPLOITATION POINT: This block always runs
# If an exception was raised, jwk_set remains None
if self.jwk_set_cache is not None:
self.jwk_set_cache.put(jwk_set) # Cache is overwritten with NoneThe remediation in version 2.13.0 completely refactors the control flow to isolate the cache-write operation from the exception handling sequence. By removing the finally block entirely, the library guarantees that the cache state is mutated only upon successful completion of both the network request and the JSON parsing routine.
# PATCHED CODE PATH (jwt/jwks_client.py)
def fetch_data(self) -> Any:
try:
r = urllib.request.Request(url=self.uri, headers=self.headers)
with urllib.request.urlopen(
r, timeout=self.timeout, context=self.ssl_context
) as response:
jwk_set = json.loads(response.read().decode("utf-8"))
except Exception as e:
raise PyJWKClientConnectionError(
f'Fail to fetch data from the url, err: "{e}"'
) from e
# REMEDIATION: Cache update occurs only after successful retrieval.
# If fetch fails, the exception is raised, and this block is bypassed.
if self.jwk_set_cache is not None:
self.jwk_set_cache.put(jwk_set)
return jwk_setWhile this patch completely resolves the cache eviction flaw (CWE-460), the design of PyJWKClient remains susceptible to network amplification. An attacker sending requests with random kid values can still force the server to initiate synchronous outbound connections. Applications utilizing PyJWKClient must configure appropriate rate limiting or implement caching strategies that penalize missing keys to prevent outbound connection exhaustion.
An exploitation scenario targeting CVE-2026-48524 does not require cryptographic material or valid user credentials. The attack leverages the target application's willingness to parse JWT headers from unauthenticated requests. The attacker must target an endpoint that relies on dynamic JWKS resolution using PyJWKClient.
The execution flow begins with the generation of automated requests containing JWTs with unique, randomized kid headers. The target application parses each header, notes that the kid is missing from the local cache, and issues a synchronous fetch request to the remote JWKS URI. This process is repeated across a high volume of concurrent threads or connections.
As the rate of outbound queries increases, the Identity Provider (e.g., Auth0 or Keycloak) detects the spike and applies rate-limiting policies, responding with an HTTP 429 Too Many Requests status code. Alternatively, the target application may exhaust its own outbound socket pool, resulting in network connection timeouts. In either case, an exception is thrown in the try block. The finally block executes immediately, writing None to the cache and wiping out the legitimate keys. Subsequent verification attempts for all users fail until a successful outbound request can be completed.
The impact of CVE-2026-48524 is classified as a Denial of Service (DoS) affecting the availability of the authentication layer. Because the vulnerability results in the eviction of legitimate public keys from the cache, any application thread trying to validate a standard user token is forced to perform a synchronous outbound request. If the upstream provider is rate-limiting the application, all incoming valid tokens will fail verification, causing a complete lockout of authorized users.
The vulnerability receives a CVSS v3.1 score of 3.7. The High attack complexity reflects the dependency on external environmental conditions, specifically the requirement that the upstream JWKS endpoint fails, rate-limits, or times out. An attacker cannot guarantee the precise timing of this failure, as it depends on network latency, connection pooling, and the rate-limiting thresholds of the specific Identity Provider.
From a business perspective, the consequences can be significant. If a critical service experiences a cache wipe, the authentication layer cannot recover until the upstream provider stops rate-limiting the application server. This creates a feedback loop: as long as the attacker continues to transmit invalid kid headers, the application will continue to trigger rate limits, extending the duration of the service outage indefinitely.
The primary remediation path is upgrading the pyjwt library to version 2.13.0 or higher. This version removes the vulnerable finally block and ensures that the cache is only updated when a network transaction is completed successfully. This prevents temporary network failures from corrupting the in-memory cache state.
If upgrading is not immediately feasible, organizations can implement several mitigation strategies. First, deploy a Web Application Firewall (WAF) or ingress controller rule to rate-limit requests to endpoints that parse JWTs. Second, implement an application-level middleware to validate the format of the kid header, discarding requests that do not match expected patterns (e.g., specific alphanumeric lengths or UUID formats used by the IdP).
To detect potential exploitation attempts, security operations teams should monitor application logging for high rates of PyJWKClientConnectionError exceptions. Additionally, network monitoring tools should track outbound connections to the JWKS endpoint. A sharp increase in outbound DNS requests or HTTP queries to the authentication provider's domain is a strong indicator of active exploitation or misconfiguration.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
pyjwt Jose Padilla | < 2.13.0 | 2.13.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-460 |
| Attack Vector | Network |
| CVSS v3.1 | 3.7 |
| EPSS Score | 0.00205 |
| Impact | Denial of Service (DoS) |
| Exploit Status | none |
| KEV Status | Not Listed |
The software does not clean up its state or resources when an exception is thrown, leading to inconsistent states (specifically, storing None in the JWKS cache when a network exception is thrown inside fetch_data()).
Flyto2 Core (flyto-core) prior to version 2.26.7 did not utilize its centralized SSRF validation mechanism ('validate_url_with_env_config') across multiple HTTP-emitting modules. This oversight allowed low-privileged users executing automated workflows to perform Server-Side Request Forgery (SSRF) attacks against internal endpoints, loopback interfaces, and cloud provider metadata services.
An SSRF vulnerability exists in Flyto2 Core due to improper validation of intermediate HTTP redirect hops. While the initial request target is validated against an SSRF protection policy, the HTTP client library (aiohttp) transparently follows 30x redirects to local, internal, or cloud metadata endpoints without application-level revalidation.
A logic vulnerability exists in @dynatrace-oss/dynatrace-mcp-server prior to version 1.8.7. The create_dynatrace_notebook tool lacks a human-approval gate, allowing an attacker to exploit indirect prompt injection to force the underlying LLM client to create persistent Dynatrace notebooks without the operator's consent.
A critical authentication and authorization bypass vulnerability in the Quarkus Java framework exists due to a parser differential mismatch between the HTTP security policy layer and downstream handlers. By leveraging encoded reserved characters such as semicolons, slashes, and backslashes, attackers can bypass configured path-based security policies to gain unauthorized access to secure administrative endpoints and static resources.
A critical code injection vulnerability exists in @aws/agentcore CLI (AWS AgentCore CLI) during the Bedrock Agent import lifecycle. An authenticated remote attacker with permissions to configure Bedrock collaborator attributes can inject python code by embedding triple-double-quotes (""") inside the collaborationInstruction metadata field. The CLI formats this metadata directly into a Python docstring in a generated main.py file without adequate escaping, leading to arbitrary code execution when the imported agent is run or deployed.
GHSA-WCHH-9X6H-7F6P documents the critical deprecation of the cryptographic library libolm (Olm) and its Python binding wrapper python-olm, which matrix-commander depended upon via its downstream client library matrix-nio. Multiple cryptographic vulnerabilities (timing leaks, side-channels, signature malleability, and protocol confusion) were disclosed in 2022 and 2024. Because libolm is unmaintained, Python clients using matrix-commander are considered cryptographically unsafe until migrating to vodozemac.