DoS via Memory Exhaustion When Decompressing Compressed Data in Tornado
DoS via Memory Exhaustion When Decompressing Compressed Data in Tornado
Title: CWE-770 (Allocation of Resources Without Limits or Throttling)
Library: Tornado (Python)
Vulnerability Type: Denial of Service (DoS)
Component: WebSocket Decompression
Affected Versions: Tornado versions <=6.1
Common Weakness Enumeration: CWE-770 (Allocation of Resources Without Limits or Throttling)
Description
In Tornado, a Python web framework and asynchronous networking library, there is a vulnerability that allows an attacker to cause Denial of Service (DoS) via memory exhaustion. This issue arises when decompressing compressed data received via HTTP or WebSocket connections. Tornado versions prior to 6.1 do not limit the size of the decompressed data, which can lead to memory exhaustion when handling maliciously crafted compressed input, such as a zip bomb.
Attack Scenario
An attacker can exploit this vulnerability by sending a specially crafted gzip-compressed payload to a Tornado web server. The payload, when decompressed, expands to a large size, consuming significant memory resources and potentially causing the server to become unresponsive.
Example Payload
A zip bomb, which is a small file that decompresses into a significantly larger file, can be used as an attack payload. For instance, a file that is a few kilobytes in size might decompress into several gigabytes.
Vulnerable Code Example
Example web app for Tornado that is vulnerable to this attack and the Fix is Add to it as Gzip Encode:
class _GzipMessageDelegate(httputil.HTTPMessageDelegate):
"""Wraps an `HTTPMessageDelegate` to decode ``Content-Encoding: gzip``.
"""
def __init__(self, delegate: httputil.HTTPMessageDelegate, chunk_size: int) -> None:
self._delegate = delegate
self._chunk_size = chunk_size
self._decompressor = None # type: Optional[GzipDecompressor]
def headers_received(
self,
start_line: Union[httputil.RequestStartLine, httputil.ResponseStartLine],
headers: httputil.HTTPHeaders,
) -> Optional[Awaitable[None]]:
if headers.get("Content-Encoding") == "gzip":
self._decompressor = GzipDecompressor()
# Downstream delegates will only see uncompressed data,
# so rename the content-encoding header.
# (but note that curl_httpclient doesn't do this).
headers.add("X-Consumed-Content-Encoding", headers["Content-Encoding"])
del headers["Content-Encoding"]
return self._delegate.headers_received(start_line, headers)
async def data_received(self, chunk: bytes) -> None:
if self._decompressor:
compressed_data = chunk
while compressed_data:
decompressed = self._decompressor.decompress(
compressed_data, self._chunk_size
)
if decompressed:
ret = self._delegate.data_received(decompressed)
if ret is not None:
await ret
compressed_data = self._decompressor.unconsumed_tail
else:
ret = self._delegate.data_received(chunk)
if ret is not None:
await ret
In the code above, there is no limit on the size of decompressed data, making it vulnerable to zip bomb attacks.
Mitigation
Standard http Content-Encoding: gzip has long been handled with a maximum decompress output size
To mitigate this issue, Tornado has added a maximum decompressed output size for handling gzip-compressed data. Below is an example of how to limit the size of decompressed data in Tornado:
In this updated code, a limit on the decompressed data size is enforced to prevent memory exhaustion attacks. The data_received method is overridden to handle decompression with a size limit.
Conclusion
To protect against Denial of Service attacks via memory exhaustion in Tornado, ensure that the size of decompressed data is limited. Upgrade to Tornado version 6.1 or later, which includes this fix, or implement similar logic in your code to handle gzip-compressed data safely.
Last updated