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

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