CVE-2026-116577: Algorithmic Complexity Denial of Service in Tornado HTTP Header Parser
Vulnerability ID: CVE-2026-116577
CVSS Score: 5.3
Published: 2026-10-28
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.
TL;DR
An algorithmic complexity flaw in Tornado's HTTP header parser (< 6.6.0) allows unauthenticated attackers to cause quadratic CPU consumption and block the asynchronous event loop by sending requests with thousands of obsolete folded header lines.
Technical Details
- Vulnerability Type: Inefficient Algorithmic Complexity (CWE-407)
- Affected Component: tornado.httputil.HTTPHeaders.parse_line
- Attack Vector: Network (Unauthenticated HTTP/1.x Request)
- CVSS v3.1 Score: 5.3 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L)
- EPSS Score: 0.38% (32.33rd Percentile)
- Security Impact: IOLoop Thread Starvation / Denial of Service
- Exploit Status: No Known Public Weaponized Exploits
- Patched Version: Tornado 6.6.0
Affected Systems
- Tornado (PyPI package: tornado)
-
Tornado: < 6.6.0 (Fixed in:
6.6.0)
Code Analysis
Commit: 1b7a1dc
httputil: Remove support for header line folding due to DoS concerns
@@ -184,7 +184,6 @@ def __init__(self, *args: typing.Any, **kwargs: str) -> None:
# on demand (and cleared whenever the list is modified).
self._as_list: dict[str, list[str]] = {}
self._combined_cache: dict[str, str] = {}
- self._last_key: str | None = None
if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], HTTPHeaders):
# Copy constructor
for k, v in args[0].get_all():
@@ -208,7 +207,6 @@ def add(self, name: str, value: str, *, _chars_are_bytes: bool = True) -> None:
if _FORBIDDEN_HEADER_CHARS_RE.search(value):
raise HTTPInputError("Invalid header value %r" % value)
norm_name = _normalize_header(name)
- self._last_key = norm_name
if norm_name in self:
self._combined_cache.pop(norm_name, None)
self._as_list[norm_name].append(value)
@@ -249,6 +247,11 @@ def parse_line(self, line: str, *, _chars_are_bytes: bool = True) -> None:
In Tornado 7.0, certain deprecated features of HTTP will become errors.
Specifically, line folding and the use of LF (with CR) as a line separator
will be removed.
+
+ .. versionchanged:: 6.6
+ Support for line folding was removed due to security/DoS concerns. This removal
+ was ahead of the original removal schedule announced for Tornado 7.0.
+ Support for LF without CR remains until Tornado 7.0.
"""
if m := re.search(r"\r?\n$", line):
# RFC 9112 section 2.2: a recipient MAY recognize a single LF as a line
@@ -259,27 +262,14 @@ def parse_line(self, line: str, *, _chars_are_bytes: bool = True) -> None:
# Empty line, or the final CRLF of a header block.
return
if line[0] in HTTP_WHITESPACE:
- # continuation of a multi-line header
- # TODO(7.0): Remove support for line folding.
- if self._last_key is None:
- raise HTTPInputError("first header line cannot start with whitespace")
- new_part = " " + line.strip(HTTP_WHITESPACE)
- if _chars_are_bytes:
- if not _ABNF.field_value.fullmatch(new_part[1:]):
- raise HTTPInputError("Invalid header continuation %r" % new_part)
- else:
- if _FORBIDDEN_HEADER_CHARS_RE.search(new_part):
- raise HTTPInputError("Invalid header value %r" % new_part)
- self._as_list[self._last_key][-1] += new_part
- self._combined_cache.pop(self._last_key, None)
- else:
- try:
- name, value = line.split(":", 1)
- except ValueError:
- raise HTTPInputError("no colon in header line")
- self.add(
- name, value.strip(HTTP_WHITESPACE), _chars_are_bytes=_chars_are_bytes
+ raise HTTPInputError(
+ "header line cannot start with whitespace, folding not supported"
)
+ try:
+ name, value = line.split(":", 1)
+ except ValueError:
+ raise HTTPInputError("no colon in header line")
+ self.add(name, value.strip(HTTP_WHITESPACE), _chars_are_bytes=_chars_are_bytes)
Mitigation Strategies
- Upgrade Tornado to version 6.6.0 or newer to reject obsolete line folding at the parser level.
- Deploy an RFC 9112-compliant reverse proxy (e.g., Nginx, Envoy, HAProxy) in front of Tornado to reject or unfold multi-line HTTP headers before they reach the Python process.
- Reduce the max_header_size parameter on Tornado HTTPServer instances from the default 64 KB to a smaller value (such as 8 KB) to limit the quadratic amplification bound.
- Run multiple Tornado worker processes behind a load balancer to isolate event loop starvation to individual worker processes.
Remediation Steps:
- Audit project dependencies (requirements.txt, pyproject.toml, Pipfile, or poetry.lock) to identify Tornado versions earlier than 6.6.0.
- Update the tornado package dependency to >= 6.6.0 using pip (
pip install --upgrade 'tornado>=6.6.0') or your preferred package manager. - Verify that no internal clients or upstream HTTP servers send obsolete multi-line folded headers, as Tornado 6.6.0 will reject them with HTTP 400.
- Redeploy the application services and verify the running Tornado version.
References
- GitHub Security Advisory: GHSA-pcg2-g6cj-q92c
- GitHub Pull Request #3765: Remove support for header line folding
- Upstream Fix Commit (1b7a1dc9f340f52de929852e8125aa12f25297c9)
- Tornado Release v6.6.0
- National Vulnerability Database: CVE-2026-116577
- CVE Record: CVE-2026-116577
- RFC 9112 Section 5.2: Obsolete Line Folding
Read the full report for CVE-2026-116577 on our website for more details including interactive diagrams and full exploit analysis.
Top comments (0)