CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-115927

CVE-2026-115927: Event Loop Blocking and Memory Amplification in Tornado Chunked Transfer Encoding Parser

Amit Schendel
Amit Schendel
Senior Security Researcher

Oct 28, 2026·11 min read·4 visits

Executive Summary (TL;DR)

An unauthenticated remote attacker can send an HTTP/1.1 request or response with millions of 1-byte chunks to a Tornado server or client (< 6.6.0), monopolizing the Python asyncio event loop and causing approximately 20x memory amplification.

Tornado versions prior to 6.6.0 contain an uncontrolled resource consumption vulnerability (CWE-400) in the HTTP/1.1 chunked transfer encoding parser. When processing incoming chunked payloads that have already buffered in the socket read buffer, Tornado parses chunks in a tight synchronous loop without yielding to the event loop. Furthermore, non-streaming request handlers and SimpleAsyncHTTPClient store each chunk as a discrete Python bytes object inside a list, inducing an approximately 20-fold memory overhead for 1-byte chunks.

Vulnerability Overview

Tornado is an asynchronous networking library and web framework for Python that utilizes a non-blocking single-threaded event loop (IOLoop, built on top of standard library asyncio) to multiplex thousands of concurrent network connections. Because all connections within a single Tornado process share the same event loop thread, the responsiveness of the entire server depends on cooperative multitasking. Every coroutine must periodically yield execution back to the scheduler so that pending I/O events and timers for other connections can be processed.

In Tornado versions prior to 6.6.0, the HTTP/1.1 protocol implementation (tornado/http1connection.py) fails to yield control to the event loop while decoding Transfer-Encoding: chunked message bodies if the underlying data is already present in the socket read buffer. When a remote peer transmits a stream of diminutive chunks—such as millions of 1-byte chunks (1\r\nx\r\n)—that arrive inside the operating system and IOStream buffers simultaneously, the chunk parser processes them in an uninterrupted synchronous loop. This behavior violates the cooperative multitasking model and blocks the event loop thread for extended durations.

In addition to CPU monopolization, the vulnerability exposes a secondary memory exhaustion vector tracked under CWE-400 (Uncontrolled Resource Consumption) and CWE-770 (Allocation of Resources Without Limits or Throttling). For standard request handlers that do not explicitly apply the @stream_request_body decorator, as well as the internal SimpleAsyncHTTPClient, Tornado accumulates incoming chunks by appending each slice as an individual bytes object into a Python list. Due to CPython object header and list pointer overhead, storing 1-byte chunks consumes approximately 120 bytes of memory per chunk, resulting in roughly 20-fold memory amplification relative to the actual HTTP body length.

Root Cause Analysis

The primary root cause of the event loop starvation lies in how Tornado's IOStream interacts with Python's await keyword when reading buffered socket data. In tornado/http1connection.py, the _read_chunked_body coroutine iterates over incoming chunks using await self.stream.read_until(b"\r\n", max_bytes=64) to read the hexadecimal chunk length header, followed by await self.stream.read_bytes(...) to read the chunk payload and trailing CRLF terminator.

In Python's asyncio architecture, executing await future only suspends the coroutine and yields control back to the event loop scheduler if the awaited Future object is currently pending (future.done() is False). When IOStream reads from the socket, it pulls available data into an internal memory buffer in bulk. If an incoming TCP segment contains thousands of pre-formatted tiny chunks, subsequent calls to stream.read_until() and stream.read_bytes() are satisfied immediately from the in-memory buffer and return pre-completed Future objects (future.done() is True). Consequently, await resolves synchronously without returning control to the IOLoop. At an average parsing cost of approximately 6 microseconds per chunk, processing millions of buffered 1-byte chunks blocks the process thread for minutes at a time.

The second technical flaw involves the data structures used to buffer chunked bodies before assembling the final HTTPServerRequest.body or HTTPResponse.body. In tornado/httpserver.py (_PerRequestAdapter), tornado/web.py (_RequestDispatcher), and tornado/simple_httpclient.py (_HTTPConnection), the data_received callback accumulated each decoded chunk directly into self._chunks = [] via self._chunks.append(chunk). Upon completion of the request, the fragments were concatenated using b"".join(self._chunks).

When _read_chunked_body invoked delegate.data_received(chunk) once per chunk, a stream of 1-byte chunks created millions of distinct 1-byte bytes objects. On 64-bit CPython, a 1-byte bytes object requires 33 bytes of allocation (object reference count, type pointer, size field, hash cache, null terminator, and 1 byte of payload), plus allocator alignment padding, an 8-byte pointer inside the dynamically resizing list array, and garbage collector tracking structures. This architecture allowed an attacker to bypass the protective intent of max_body_size by multiplying memory consumption by a factor of roughly 20.

> [!NOTE] > A third minor defect existed in the chunk termination check within _read_chunked_body. Tornado verified the trailing \r\n delimiter after each chunk payload using assert crlf == b"\r\n". When Python is executed with optimization flags (-O or PYTHONOPTIMIZE=1), assert statements are stripped from the bytecode, causing malformed chunk terminators to be ignored silently. In unoptimized runtimes, invalid terminators raised an unhandled AssertionError rather than a catchable httputil.HTTPInputError.

Code Analysis

Commit ceab33581489751a66fba8566ad6d5960d4b01c8 resolves both the event loop starvation and memory amplification vulnerabilities across tornado/http1connection.py, tornado/httpserver.py, tornado/web.py, and tornado/simple_httpclient.py.

Prior to the patch, _read_chunked_body in tornado/http1connection.py processed chunks in a direct loop without tracking synchronous execution time or coalescing small reads:

# Vulnerable implementation in tornado/http1connection.py (prior to 6.6.0)
async def _read_chunked_body(self, delegate: httputil.HTTPMessageDelegate) -> None:
    total_size = 0
    while True:
        chunk_len_str = await self.stream.read_until(b"\r\n", max_bytes=64)
        try:
            chunk_len = int(chunk_len_str.strip(), 16)
        except ValueError:
            raise httputil.HTTPInputError("invalid chunk size")
        if chunk_len == 0:
            break
        total_size += chunk_len
        if total_size > self._max_body_size:
            raise httputil.HTTPInputError("chunked body too large")
        bytes_to_read = chunk_len
        while bytes_to_read:
            chunk = await self.stream.read_bytes(
                min(bytes_to_read, self.params.chunk_size), partial=True
            )
            bytes_to_read -= len(chunk)
            if not self._is_client and self.stream.closed():
                return
            ret = delegate.data_received(chunk)
            if ret is not None:
                await ret
        # assert is stripped under python -O and raises AssertionError instead of HTTPInputError
        crlf = await self.stream.read_bytes(2)
        assert crlf == b"\r\n"

The patched implementation in tornado/http1connection.py introduces two module-level constants, _YIELD_THRESHOLD = 256 * 1024 (256 KB) and _CHUNK_YIELD_COST = 1024 (1 KB), alongside a local bytearray buffer (pending) to coalesce tiny chunks and periodically force an event loop turn via await asyncio.sleep(0):

# Patched implementation in tornado/http1connection.py (version 6.6.0)
_YIELD_THRESHOLD = 256 * 1024
_CHUNK_YIELD_COST = 1024
 
async def _read_chunked_body(self, delegate: httputil.HTTPMessageDelegate) -> None:
    total_size = 0
    pending = bytearray()
    work = 0
 
    async def flush() -> None:
        if not pending:
            return
        data = bytes(pending)
        pending.clear()
        ret = delegate.data_received(data)
        if ret is not None:
            await ret
 
    async def read(future: "Future[bytes]") -> bytes:
        nonlocal work
        if not _is_done(future):
            # Stream must wait on network I/O; flush buffered chunks immediately
            await flush()
            work = 0
        return await future
 
    while True:
        chunk_len_ bytes = await read(
            self.stream.read_until(b"\r\n", max_bytes=64)
        )
        work += _CHUNK_YIELD_COST  # Penalize each chunk header by 1 KB virtual weight
        # ... [chunk_len parsing and max_body_size checks] ...
        bytes_to_read = chunk_len
        while bytes_to_read:
            chunk = await read(
                self.stream.read_bytes(
                    min(bytes_to_read, self.params.chunk_size), partial=True
                )
            )
            bytes_to_read -= len(chunk)
            pending += chunk
            work += len(chunk)
            if len(pending) >= self.params.chunk_size:
                await flush()
            if work >= _YIELD_THRESHOLD:
                work = 0
                await asyncio.sleep(0)  # Force yield to IOLoop scheduler
        crlf = await read(self.stream.read_bytes(2))
        if crlf != b"\r\n":
            raise httputil.HTTPInputError("improperly terminated chunk")
        if work >= _YIELD_THRESHOLD:
            work = 0
            await asyncio.sleep(0)
    await flush()

To address the memory amplification in non-streaming handlers and the HTTP client, tornado/httpserver.py, tornado/web.py, and tornado/simple_httpclient.py were updated to replace list[bytes] with a contiguous bytearray() buffer:

# Patched chunk accumulation in tornado/httpserver.py, tornado/web.py, and tornado/simple_httpclient.py
# Before:
# self._chunks: list[bytes] = []
# self._chunks.append(chunk)
# self.request.body = b"".join(self._chunks)
 
# After:
self._chunks = bytearray()
# In data_received:
self._chunks += chunk
# In finish:
self.request.body = bytes(self._chunks)

Fix Completeness Assessment

The fix is comprehensive and addresses all identified aspects of the vulnerability. By assigning a synthetic cost of _CHUNK_YIELD_COST (1,024 bytes) to every chunk header, the parser guarantees that await asyncio.sleep(0) is executed at least once every 256 chunks, even for 1-byte payloads (~1.5 ms of CPU time per yield). Simultaneously, coalescing chunks into pending = bytearray() and replacing self._chunks with bytearray() eliminates per-chunk Python object overhead for both streaming and non-streaming handlers. The _read_fixed_body method was similarly updated with _YIELD_THRESHOLD accounting to ensure large buffered fixed-length bodies also yield periodically.

Exploitation

Exploiting CVE-2026-115927 does not require authentication, special privileges, or complex state manipulation. The vulnerability can be triggered against any internet-facing Tornado HTTP/1.1 server directly, or against a client application utilizing SimpleAsyncHTTPClient if the client can be induced to connect to an attacker-controlled HTTP server.

Server-Side Exploitation

To attack a Tornado server, a client establishes a standard TCP connection and sends an HTTP request (such as POST / HTTP/1.1) containing the header Transfer-Encoding: chunked. Instead of sending body data in standard buffer sizes, the attacker fills the TCP send window with consecutive 6-byte sequences representing 1-byte chunks: 1\r\nx\r\n (0x31 0x0d 0x0a 0x78 0x0d 0x0a).

When these packets arrive at the server and are read into the IOStream buffer, Tornado enters _read_chunked_body. With the default max_body_size of 100 MB (104,857,600 bytes), a single request can legally contain up to 104,857,600 individual 1-byte chunks before exceeding the body size limit. Even if an attacker sends a much smaller payload—such as 1,000,000 chunks (approximately 6 MB of wire traffic)—the server will spend several seconds continuously executing the loop without yielding to IOLoop, halting all concurrent requests on that worker process and allocating roughly 120 MB of heap memory if the target route does not use @stream_request_body.

Proof-of-Concept Analysis

The regression test added in commit ceab33581489751a66fba8566ad6d5960d4b01c8 (tornado/test/http1connection_test.py) demonstrates the mechanism used to verify event loop starvation:

# Adapted from tornado/test/http1connection_test.py (test_chunked_body_tiny_chunks)
num_chunks = 5000
payload = (
    b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
    + b"1\r\nx\r\n" * num_chunks
    + b"0\r\n\r\n"
)
# Write the entire chunked stream into the socket buffer at once
yield self.server_stream.write(payload)
 
# Background coroutine that increments `ticks` on each event loop turn
ticks = 0
running = True
async def ticker():
    nonlocal ticks
    while running:
        await asyncio.sleep(0)
        ticks += 1
 
asyncio.ensure_future(ticker())
yield conn.read_response(delegate)
running = False
 
# On vulnerable Tornado (< 6.6.0), `ticks` remains 0 because read_response
# processes all 5,000 buffered chunks synchronously without yielding.
# On patched Tornado (>= 6.6.0), `ticks` is >= 10.

Impact Assessment

The primary security impact of CVE-2026-115927 is denial of service (DoS) affecting availability, scored at CVSS v3.1 5.3 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L). There is no direct impact on confidentiality (C:N) or integrity (I:N), as the flaw does not permit out-of-bounds memory reads, buffer overwrites, or HTTP request smuggling.

In production deployments, the impact severity depends heavily on the deployment architecture:

  • Directly Exposed Tornado Servers: If Tornado listens directly on public network interfaces (or sits behind a load balancer operating in Layer 4 TCP passthrough mode or Layer 7 streaming mode without request buffering), a single unauthenticated attacker can repeatedly stall the event loop of each worker process. Health check probes from container orchestrators (e.g., Kubernetes liveness probes) will time out while the event loop is blocked, potentially triggering cascading container restarts.
  • Memory Exhaustion: On endpoints lacking @stream_request_body, sending 10 MB of 1-byte chunks (60 MB on the wire) forces the Python process to allocate roughly 1.2 GB of RAM for the self._chunks list, risking Out-Of-Memory (OOM) termination by the operating system kernel.
  • Client-Side Applications: Applications that use tornado.simple_httpclient.SimpleAsyncHTTPClient to fetch user-supplied URLs (e.g., webhook dispatchers, web scrapers, or federated services) are vulnerable to the exact same event loop freeze and memory amplification if a remote server replies with a malicious chunked response.

As of October 2026, the Exploit Prediction Scoring System (EPSS) assigns this vulnerability a score of 0.00384 (32.33rd percentile), and it is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.

Remediation

Patch Guidance

Administrators and developers should upgrade Tornado to version 6.6.0 or later, which includes commit ceab33581489751a66fba8566ad6d5960d4b01c8.

pip install --upgrade "tornado>=6.6.0"

Workarounds and Mitigations

If an immediate upgrade to Tornado 6.6.0 is not feasible, the following architectural mitigations reduce exposure:

  1. Enable Request Buffering on Upstream Reverse Proxies: Place Tornado behind an HTTP reverse proxy such as NGINX, HAProxy, or Envoy configured to buffer incoming client request bodies before forwarding them to the upstream Tornado backend. For example, in NGINX, proxy_request_buffering on; is enabled by default. When active, NGINX reads the entire chunked payload from the client and forwards it to Tornado either with a fixed Content-Length header or in large coalesced buffers, neutralizing the tiny-chunk attack vector on the server side.
  2. Restrict max_body_size: While lowering max_body_size on HTTPServer or AsyncHTTPClient does not prevent synchronous loop execution while chunks are being read, reducing it to the minimum required by the application bounds both the maximum number of 1-byte chunks allowed per request and the peak memory amplification.
  3. Enforce Outbound Egress Controls for SimpleAsyncHTTPClient: If the application uses SimpleAsyncHTTPClient to communicate with external servers, ensure it only connects to trusted endpoints, or switch to tornado.curl_httpclient.CurlAsyncHTTPClient (which delegates HTTP transport parsing to libcurl rather than tornado/http1connection.py).

Official Patches

tornadowebOfficial patch commit in tornadoweb/tornado repository
tornadowebTornado v6.6.0 Release Notes
tornadowebGitHub Security Advisory GHSA-r4j5-9xpp-3985

Fix Analysis (1)

Technical Appendix

CVSS Score
5.3/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
EPSS Probability
0.38%
Top 68% most exploited

Affected Systems

Tornado Web Framework (< 6.6.0)Tornado HTTPServer (< 6.6.0)Tornado SimpleAsyncHTTPClient (< 6.6.0)

Affected Versions Detail

Product
Affected Versions
Fixed Version
tornado
tornadoweb
< 6.6.06.6.0
AttributeDetail
CVE IDCVE-2026-115927 (GHSA-r4j5-9xpp-3985)
CWE ClassificationCWE-400 (Uncontrolled Resource Consumption) / CWE-770
CVSS v3.1 Score5.3 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L)
Attack VectorNetwork (Unauthenticated HTTP/1.1 Chunked Transfer Encoding)
Affected Componenttornado/http1connection.py, httpserver.py, web.py, simple_httpclient.py
Security ImpactProcess-wide IOLoop starvation and ~20x memory amplification
EPSS Score0.384% (32.33rd percentile)
Patched VersionTornado 6.6.0

MITRE ATT&CK Mapping

T1499Endpoint Denial of Service
Impact
CWE-400
Uncontrolled Resource Consumption

The software does not properly control the allocation or maintenance of a limited system resource, enabling an actor to consume more resources than intended.

Known Exploits & Detection

GitHubUnit test reproduction in tornado/test/http1connection_test.py (test_chunked_body_tiny_chunks) demonstrating event loop starvation with 1-byte chunks.

Vulnerability Timeline

Security patch commit ceab33581489751a66fba8566ad6d5960d4b01c8 authored and merged via Pull Request #3773.
2026-10-07
GitHub Security Advisory GHSA-r4j5-9xpp-3985 and CVE-2026-115927 published.
2026-10-26
NVD record published with CVSS v3.1 base score of 5.3.
2026-10-26
CVE and NVD records updated with v6.6.0 release notes and references.
2026-10-27

References & Sources

  • [1]NVD - CVE-2026-115927
  • [2]CVE.org Record - CVE-2026-115927
  • [3]GitHub Security Advisory GHSA-r4j5-9xpp-3985
  • [4]Tornado Pull Request #3773
  • [5]Tornado Patch Commit ceab33581489751a66fba8566ad6d5960d4b01c8
  • [6]Tornado v6.6.0 Release Tag

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•8 minutes ago•CVE-2026-116577
5.3

CVE-2026-116577: Algorithmic Complexity Denial of Service in Tornado HTTP Header Parser

Tornado versions prior to 6.6.0 contain an inefficient algorithmic complexity vulnerability in the HTTP header parser. When processing requests with obsolete line folding (obs-fold), the HTTPHeaders.parse_line method repeatedly concatenates immutable Python strings within a list structure. An unauthenticated remote attacker can send an HTTP request containing thousands of short header continuation lines to trigger quadratic O(N^2) CPU consumption on the main IOLoop thread, starving concurrent connections and resulting in a denial of service.

Alon Barad
Alon Barad
0 views•10 min read
•about 1 hour ago•CVE-2026-116578
2.1

CVE-2026-116578: Open Redirect in Tornado RedirectHandler via URL Normalization and Template Substitution

Tornado versions prior to 6.6.0 contain an open redirect vulnerability (CWE-601) in tornado.web.RedirectHandler. When route templates incorporate regular expression capture groups at the start of a path, directly after a fixed host without a trailing slash, or at the beginning of a destination template, unvalidated URL-decoded parameters allow attackers to manipulate the redirect scheme or network location. Because modern browsers normalize URLs according to the WHATWG specification—treating backslashes as forward slashes and stripping control characters—attackers can bypass naive prefix checks and redirect victims to arbitrary external domains.

Alon Barad
Alon Barad
2 views•11 min read
•about 3 hours ago•CVE-2026-117601
9.3

CVE-2026-117601: Improper Cryptographic Signature Verification in Electrum Bitcoin Wallet

A missing cryptographic signature verification vulnerability exists in Electrum Bitcoin Wallet versions prior to 4.8.3. In Electrum's Simplified Payment Verification (SPV) architecture, the client failed to validate input signatures on unconfirmed mempool transactions received from connected Electrum servers. A malicious Electrum server can exploit this flaw by intercepting a legitimate user broadcast, suppressing it, and returning a spoofed unconfirmed transaction with an invalid signature and zero fee that redirects the user's UTXOs to an attacker-controlled address. When the user initiates a Replace-By-Fee (RBF) action to accelerate the seemingly stuck transaction, Electrum signs the spoofed outputs with the user's private keys, resulting in the direct theft of funds.

Amit Schendel
Amit Schendel
8 views•10 min read
•about 4 hours ago•CVE-2026-118254
8.6

CVE-2026-118254: DOM-based Cross-Site Scripting in Highcharts Grid Pagination Module

CVE-2026-118254 (GHSA-r5wg-q8fc-p6w2) is a high-severity DOM-based Cross-Site Scripting (DOM XSS) vulnerability in the pagination module of Highcharts Grid (@highcharts/grid-lite and @highcharts/grid-pro). The flaw stems from the direct assignment of localization configuration strings (lang.pageInfo and lang.pageSizeLabel) and pagination UI labels to DOM elements via the innerHTML property instead of text-safe DOM APIs such as textContent or innerText. When an application passes attacker-controlled data into the Grid localization options with pagination enabled, arbitrary HTML and JavaScript execute automatically within the hosting document's origin context upon component rendering.

Amit Schendel
Amit Schendel
7 views•10 min read
•about 5 hours ago•CVE-2026-118255
8.4

CVE-2026-118255: Cross-Site Scripting via Unfiltered Credits Href in Highcharts Grid

CVE-2026-118255 is a Cross-Site Scripting (XSS) vulnerability in Highcharts Grid and Highcharts Grid Pro. The flaw occurs because the credits component assigns the configuration-supplied credits.href property directly to an HTML anchor element without verifying the URI scheme against the internal Abstract Syntax Tree (AST) allowlist. An attacker who can influence the grid configuration can supply a javascript: URI, resulting in arbitrary JavaScript execution in the victim's browser session when the credits link is clicked.

Amit Schendel
Amit Schendel
8 views•9 min read
•about 10 hours ago•GHSA-F897-62HF-6GXQ
7.5

GHSA-f897-62hf-6gxq: Unbounded Request Body and JSON-RPC Batch DoS in Model Context Protocol TypeScript SDK

The Model Context Protocol (MCP) TypeScript SDK contains an uncontrolled resource consumption vulnerability in its HTTP server transport implementations and middleware adapters. Affected versions fail to enforce upper bounds on incoming HTTP POST request body sizes and JSON-RPC 2.0 batch array lengths. In addition, middleware wrappers for Express and Hono parse request bodies into memory prior to validating Host and Origin headers. Unauthenticated remote attackers can exploit these flaws to trigger Out-Of-Memory (OOM) process crashes or monopolize the single-threaded Node.js event loop.

Amit Schendel
Amit Schendel
6 views•11 min read