Oct 1, 2026·6 min read·5 visits
A design asymmetry in Tornado's query-string parser allows unbounded key-value field processing on GET requests, allowing remote attackers to starve CPU resources and block the event loop, causing a denial of service.
An uncontrolled resource consumption vulnerability in Tornado's HTTP query-string parser allows remote, unauthenticated attackers to trigger CPU exhaustion and block the single-threaded event loop via crafted request URIs containing large numbers of parameters.
The Tornado web server is an asynchronous networking library and web framework built to run on a single-threaded event loop known as the IOLoop. In an event-loop-driven system, synchronous, CPU-bound tasks block the execution of all other concurrent connections. If any system component executes an expensive synchronous operation, the entire application freezes for the duration of that task.
To mitigate CPU-exhaustion vectors, the Tornado framework introduced restrictions on incoming payloads. In version 6.5.8, a restriction was added to limit the number of fields in URL-encoded POST bodies to 1,000 fields using the max_num_fields parameter. This effectively protected the application against body-based multi-field exploitation.
However, the HTTP GET query-string parsing logic was not restricted. Because query strings bypass the POST-body parser and funnel directly to the initial request initialization parser, this path remained entirely unbounded. Unauthenticated remote attackers can leverage this unconstrained execution path to send requests with thousands of query-string parameters.
The core of the vulnerability lies in the asymmetrical implementation of input validation between the POST body parser and the query-string parser inside tornado/httputil.py. While the POST path calls parse_body_arguments with an explicit max_num_fields argument, the GET path initializes arguments within HTTPServerRequest.__init__ using an unconstrained call.
Under the hood, Tornado maps query-string parsing to tornado.escape.parse_qs_bytes, which wraps Python's standard library utility urllib.parse.parse_qsl. Python's standard parser is highly CPU-intensive when handling a massive quantity of fields. The parsing algorithm involves repetitive dictionary lookups, string splitting, and URL-decoding logic that exhibits superlinear execution costs as parameter counts rise.
Although Tornado enforces a global default header limit of 64KB (max_header_size), an attacker can craft highly dense payloads containing thousands of minimal keys. A 64KB payload can pack up to approximately 12,700 individual key-value pairs. This volume is more than sufficient to introduce blocking latency on the single-threaded event loop, which cannot process new events while parsing the query.
A direct comparison of the vulnerable and patched file tornado/httputil.py reveals the exact mechanism of the fix. In the unpatched version, the request constructor processes the incoming query string without passing any boundary limits.
# Vulnerable implementation in Tornado < 6.5.9
if uri is not None:
self.path, sep, self.query = uri.partition("?")
self.arguments = parse_qs_bytes(self.query, keep_blank_values=True)The patched version integrates boundary defense by pulling the global limit from _DEFAULT_PARSE_BODY_CONFIG and wrapping the initialization in a try-except block to gracefully handle exceptions as HTTP 400 errors.
# Patched implementation in Tornado 6.5.9
if uri is not None:
self.path, sep, self.query = uri.partition("?")
try:
self.arguments = parse_qs_bytes(
self.query,
keep_blank_values=True,
# The query string is bounded by max_header_size, but parsing
# is expensive enough per field to be worth limiting. Use the
# same limit as a urlencoded body. This reads the global
# config because HTTPServerRequest has no access to the
# per-connection configuration; see set_parse_body_config.
max_num_fields=_DEFAULT_PARSE_BODY_CONFIG.urlencoded.max_arguments,
)
except Exception as e:
raise HTTPInputError("Invalid query string: %s" % e) from eWhile this patch is highly effective for standard deployments, certain limitations exist. If downstream developers manually access the raw self.request.query and pass it to unconstrained third-party parsers, they remain vulnerable to CPU exhaustion. Additionally, because the query string parser lacks direct access to the per-connection configuration, it relies entirely on the global config default, meaning administrators cannot configure customized query-string limits per handler.
To execute this denial of service attack, an unauthenticated remote attacker identifies a public interface exposing a standard handler. The attacker then structures a request line with an extremely high concentration of parameters, such as GET /?k0=1&k1=1&k2=1... up to thousands of parameters.
Empirical verification conducted against an unpatched instance shows that standard baseline probes to /?a=1 require approximately 1.86ms of processing latency. When a payload of 7,800 parameters (~61KB) is sent to the server, the parsing processing latency rises to an average of 25.1ms, which represents an increase of over 13 times the baseline duration.
When multiple concurrent requests are initiated, the compounding latency starves the IOLoop completely. In dynamic testing, sending five concurrent exploit requests caused legitimate baseline requests to face an average latency of 13.0ms, with peak latency reaching 118.1ms. If the default max_header_size is increased from 64KB, the execution time scales exponentially, allowing a single request to lock the CPU for several seconds.
The security impact of GHSA-3HV7-MJH2-FV65 is classified as a low-to-medium availability threat with a CVSS score of 5.3. The attack can be executed remotely without privileges, bypasses internal authentication layers, and does not require user interaction.
Although the flaw does not lead to remote code execution, information leakage, or memory corruption, its impact on application availability is immediate. Because Tornado is single-threaded, a steady, low-bandwidth stream of high-density requests can degrade performance to a state where standard connections timeout.
Organizations running latency-critical web services, real-time message brokers, or WebSockets on unpatched Tornado versions are vulnerable. Under sustained exploitation, upstream reverse proxies and load balancers may mark the unresponsive Tornado upstream nodes as unhealthy, resulting in complete service dropouts.
The primary recommendation is to update the Tornado framework package to version 6.5.9 or higher. This update restricts query strings to 1,000 arguments and rejects oversized payloads with a 400 Bad Request error.
If upgrading immediately is not an option, you can implement reverse proxy mitigations to shield the upstream server. For instance, in an Nginx deployment, keep the large_client_header_buffers directive strict, or add configuration to reject queries exceeding a specific length:
if ($query_string ~ "^.{8192,}") {
return 414;
}Additionally, developers can programmatically limit the maximum permissible header size in the Tornado HTTP server instantiation block, which minimizes the capacity of the vector:
# Restrict maximum header size to 16KB to reduce parameters payload density
http_server = tornado.httpserver.HTTPServer(app, max_header_size=16384)CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
tornado Tornado | < 6.5.9 | 6.5.9 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-400 (Uncontrolled Resource Consumption) / CWE-407 (Algorithmic Complexity) |
| Attack Vector | Network (Unauthenticated) |
| CVSS v3.1 Score | 5.3 (Medium) |
| EPSS Score | Not Applicable (requires CVE ID) |
| Exploit Status | PoC (Proof-of-Concept) |
| CISA KEV Status | Not Applicable |
| Impact | Denial of Service (DoS) |
The system does not adequately limit the allocation of CPU resources when processing multi-field query-string inputs.
An improper neutralization of input during web page generation (CWE-79) vulnerability exists in the server-side rendering JSX engine (hono/jsx) of the Hono web framework prior to version 4.13.7. The flaw enables unauthenticated remote attackers to execute arbitrary JavaScript in the victim's browser context by supplying unescaped HTML characters into user-controlled fields rendered within specific boundary components, context providers, or direct server-side utilities.
A path traversal vulnerability exists in GitPython when handling submodule updates recursively. If an attacker crafts a malicious repository with traversed paths or symbolic links in the submodule configuration, they can execute arbitrary file writes outside the parent repository's working directory. This can lead to system configuration modifications or arbitrary code execution.
A path traversal and arbitrary file disclosure vulnerability exists in Tornado's StaticFileHandler. In versions prior to 6.5.9, the handler follows symbolic links that point outside of the configured root static directory. This behavior occurs because the handler performs lexical path validation rather than physical filesystem resolution, allowing unauthenticated remote attackers to read arbitrary files if they can access or control symbolic links within the served static root.
A critical uncontrolled resource consumption vulnerability exists in the Tornado web server's libcurl-based HTTP client (CurlAsyncHTTPClient). When processing highly compressed responses with response decompression enabled, the client experiences unbounded memory growth. This leads to host memory exhaustion and denial of service via application crashes.
PyJWT versions 2.11.0 through 2.13.0 suffer from a state pollution vulnerability in the `_merge_options` method. When an application passes a mutable configuration mapping with signature verification disabled, the library modifies the object in-place. If this same dictionary is reused for subsequent verified decode operations, standard claim verifications (such as expiration, audience, and issuer validation) are silently bypassed.
A protocol validation vulnerability exists in Fastify before version 5.12.5. When serving requests over HTTP/2, Fastify unconditionally injects the forbidden Transfer-Encoding header when response trailers are used, triggering an uncaught exception in Node.js and crashing the process.