Summary
Tornado: Unbounded query-string argument count allows event-loop-stalling DoS
HTTPServerRequest.__init__ in tornado/httputil.py parses the URL query string viaparse_qs_bytes() with no field-count limit, while the sibling POST-body parsing path
(parse_body_arguments) received a max_num_fields=1000 cap added earlier in this exact
same release (v6.5.8, commit 8d6363ed), explicitly to bound parsing cost for the identical
underlying primitive. This leaves the query-string path with the resource-exhaustion exposure
the body-path fix was meant to close.
File: tornado/httputil.py, line 553 (HTTPServerRequest.__init__)
Root Cause
# tornado/httputil.py:553 (before fix)
self.arguments = parse_qs_bytes(self.query, keep_blank_values=True)
Compare with the POST-body path fixed one commit earlier in the same release:
# tornado/httputil.py:1038-1041
uri_arguments = parse_qs_bytes(
body,
keep_blank_values=True,
max_num_fields=config.urlencoded.max_arguments, # default 1000
)
Both call sites funnel through the same tornado.escape.parse_qs_bytes (a thin wrapper overurllib.parse.parse_qs), which is exactly why max_num_fields was added tourllib.parse.parse_qsl upstream, to let frameworks bound field count. The fix was applied
only to the body path; the query-string path was missed.
The request line + headers together are capped at max_header_size (default 65536 bytes), so
this is not literally unbounded, but a single ~64KB request line can carry thousands of shortkey=value pairs, far beyond the 1000-field limit the maintainer judged appropriate for the
structurally identical body case.
Attack Scenario
- Attacker sends a
GETrequest whose query string is packed with thousands of short fields
(e.g.k0=1&k1=1&...&k7799=1, ~61KB), fitting comfortably undermax_header_size. No
authentication, cookies, or prior state required. - Tornado accepts and parses this with no field-count cap, unlike the equivalent POST-body
request (which is correctly rejected with 400 once >1000 fields are present). - Parsing thousands of fields is CPU work performed synchronously inside Tornado's
single-threadedIOLoop. Several such requests in flight concurrently stall the event
loop, delaying processing of all other connections on that loop, not just the
attacker's own request.
Verification (dynamic, local reproduction against v6.5.8)
Ran the unmodified v6.5.8 source directly (no external dependencies needed) with a minimaltornado.web.Application on 127.0.0.1:8888.
- Identical 7800-field/~61KB payload sent as GET query string →
200 OK; sent as POST body
(application/x-www-form-urlencoded) →400 Bad Request(correctly rejected by the
existingmax_num_fieldsbody-path limit). This confirms the asymmetry directly. - Per-request parse cost: baseline (
/?a=1) averaged 1.86ms; the 7800-field query string
averaged 25.1ms (~13x). - Event-loop-blocking amplification (raw-socket test, isolating server-side stall from
client overhead): with 10 sequential baseline probe requests fired with no load, average
latency was 1.47ms (max 5.9ms). With 5 concurrent 61KB/7800-field requests in flight,
the same baseline probes averaged 13.0ms (max 118.1ms), an 8.9x average slowdown for
unrelated clients, produced by ~305KB of unauthenticated attacker traffic.
Impact
All Tornado servers/applications are affected, this triggers on every request with a query
string, independent of application/handler logic. An unauthenticated, unprivileged remote
attacker can measurably degrade response times for all other clients sharing the sameIOLoop, using a small amount of bandwidth and no special conditions. This is an
availability/DoS concern; no confidentiality or integrity impact.
The application allocates resources such as memory, threads, or file descriptors based on untrusted input without enforcing a cap. Typical impact: resource exhaustion leading to denial of service.
GHSA-3HV7-MJH2-FV65 has a CVSS score of 5.3 (Medium). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (6.5.9); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
# tornado/httputil.py, HTTPServerRequest.__init__
if uri is not None:
self.path, sep, self.query = uri.partition("?")
try:
self.arguments = parse_qs_bytes(
self.query,
keep_blank_values=True,
max_num_fields=_DEFAULT_PARSE_BODY_CONFIG.urlencoded.max_arguments,
)
except ValueError as e:
raise HTTPInputError("Invalid query string: %s" % e) from e
This reuses the existing ParseUrlEncodedConfig.max_arguments default (1000) via the
module's _DEFAULT_PARSE_BODY_CONFIG, matching the POST-body limit and honoring any global
override via set_parse_body_config(). The try/except is necessary because, unlikeparse_body_arguments, which already wraps its call and converts ValueError into a cleanHTTPInputError/400, the query-string call site currently has no such handling, so without
it, a request exceeding the limit would raise an uncaught ValueError instead of a clean 400.
Verified: with the fix applied, requests with ≤1000 query-string fields are unaffected;
requests with >1000 fields are rejected with 400 Bad Request (consistent with the POST-body
behavior); Tornado's own httputil_test and web_test suites (256 tests) pass unchanged.
Frequently Asked Questions
- What is GHSA-3HV7-MJH2-FV65? GHSA-3HV7-MJH2-FV65 is a medium-severity allocation of resources without limits or throttling vulnerability in tornado (pip), affecting versions <= 6.5.8. It is fixed in 6.5.9. The application allocates resources such as memory, threads, or file descriptors based on untrusted input without enforcing a cap.
- How severe is GHSA-3HV7-MJH2-FV65? GHSA-3HV7-MJH2-FV65 has a CVSS score of 5.3 (Medium). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of tornado are affected by GHSA-3HV7-MJH2-FV65? tornado (pip) versions <= 6.5.8 is affected.
- Is there a fix for GHSA-3HV7-MJH2-FV65? Yes. GHSA-3HV7-MJH2-FV65 is fixed in 6.5.9. Upgrade to this version or later.
- Is GHSA-3HV7-MJH2-FV65 exploitable, and should I be worried? Whether GHSA-3HV7-MJH2-FV65 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether GHSA-3HV7-MJH2-FV65 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix GHSA-3HV7-MJH2-FV65? Upgrade
tornadoto 6.5.9 or later.