File CVE-2025-67725.patch of Package python-tornado.42132

From 68e81b4a3385161877408a7a49c7ed12b45a614d Mon Sep 17 00:00:00 2001
From: Ben Darnell <ben@bendarnell.com>
Date: Tue, 9 Dec 2025 13:27:27 -0500
Subject: [PATCH] httputil: Fix quadratic performance of repeated header lines

Previouisly, when many header lines with the same name were found
in an HTTP request or response, repeated string concatenation would
result in quadratic performance. This change does the concatenation
lazily (with a cache) so that repeated headers can be processed
efficiently.

Security: The previous behavior allowed a denial of service attack
via a maliciously crafted HTTP message, but only if the
max_header_size was increased from its default of 64kB.
---
 tornado/httputil.py           | 36 ++++++++++++++++++++++++-----------
 tornado/test/httputil_test.py | 15 +++++++++++++++
 2 files changed, 40 insertions(+), 11 deletions(-)

Index: tornado-4.2.1/tornado/httputil.py
===================================================================
--- tornado-4.2.1.orig/tornado/httputil.py
+++ tornado-4.2.1/tornado/httputil.py
@@ -129,6 +129,7 @@ class HTTPHeaders(dict):
         # our __setitem__
         dict.__init__(self)
         self._as_list = {}
+        self._combined_cache = {}
         self._last_key = None
         if (len(args) == 1 and len(kwargs) == 0 and
                 isinstance(args[0], HTTPHeaders)):
@@ -146,10 +147,7 @@ class HTTPHeaders(dict):
         norm_name = _normalized_headers[name]
         self._last_key = norm_name
         if norm_name in self:
-            # bypass our override of __setitem__ since it modifies _as_list
-            dict.__setitem__(self, norm_name,
-                             native_str(self[norm_name]) + ',' +
-                             native_str(value))
+            self._combined_cache.pop(norm_name, None)
             self._as_list[norm_name].append(value)
         else:
             self[norm_name] = value
@@ -181,8 +179,7 @@ class HTTPHeaders(dict):
             # continuation of a multi-line header
             new_part = ' ' + line.lstrip()
             self._as_list[self._last_key][-1] += new_part
-            dict.__setitem__(self, self._last_key,
-                             self[self._last_key] + new_part)
+            self._combined_cache.pop(self._last_key, None)
         else:
             name, value = line.split(":", 1)
             self.add(name, value.strip())
@@ -205,20 +202,24 @@ class HTTPHeaders(dict):
 
     def __setitem__(self, name, value):
         norm_name = _normalized_headers[name]
-        dict.__setitem__(self, norm_name, value)
+        self._combined_cache[norm_name] = value
         self._as_list[norm_name] = [value]
 
     def __getitem__(self, name):
-        return dict.__getitem__(self, _normalized_headers[name])
+        header = _normalized_headers[name]
+        if header not in self._combined_cache:
+            self._combined_cache[header] = ",".join(self._as_list[header])
+        return self._combined_cache[header]
 
     def __delitem__(self, name):
         norm_name = _normalized_headers[name]
-        dict.__delitem__(self, norm_name)
+        del self._combined_cache[norm_name]
         del self._as_list[norm_name]
 
     def __contains__(self, name):
-        norm_name = _normalized_headers[name]
-        return dict.__contains__(self, norm_name)
+        if not isinstance(name, str):
+            return False
+        return name in self._as_list
 
     def get(self, name, default=None):
         return dict.get(self, _normalized_headers[name], default)
@@ -232,6 +233,12 @@ class HTTPHeaders(dict):
         # default implementation returns dict(self), not the subclass
         return HTTPHeaders(self)
 
+    def __len__(self):
+        return len(self._combined_cache)
+
+    def __iter__(self):
+        return iter(self._as_list)
+
     # Use our overridden copy method for the copy.copy module.
     __copy__ = copy
 
Index: tornado-4.2.1/tornado/test/httputil_test.py
===================================================================
--- tornado-4.2.1.orig/tornado/test/httputil_test.py
+++ tornado-4.2.1/tornado/test/httputil_test.py
@@ -301,6 +301,21 @@ Foo: even
             self.assertIsNot(headers, h1)
             self.assertIsNot(headers.get_list('A'), h1.get_list('A'))
 
+    def test_linear_performance(self):
+        def f(n):
+            start = time.time()
+            headers = HTTPHeaders()
+            for i in range(n):
+                headers.add("X-Foo", "bar")
+            return time.time() - start
+
+        # This runs under 50ms on my laptop as of 2025-12-09.
+        d1 = f(10000)
+        d2 = f(100000)
+        if d2 / d1 > 20:
+            # d2 should be about 10x d1 but allow a wide margin for variability.
+            self.fail("HTTPHeaders.add() does not scale linearly: %s vs %s" % (d1, d2))
+
 
 
 class FormatTimestampTest(unittest.TestCase):
@@ -342,6 +357,20 @@ class HTTPServerRequestTest(unittest.Tes
         requets = HTTPServerRequest(uri='/')
         self.assertIsInstance(requets.body, bytes)
 
+    def test_linear_performance(self):
+        def f(n):
+            start = time.time()
+            headers = HTTPHeaders()
+            for i in range(n):
+                headers.add("X-Foo", "bar")
+            return time.time() - start
+
+        # This runs under 50ms on my laptop as of 2025-12-09.
+        d1 = f(10000)
+        d2 = f(100000)
+        if d2 / d1 > 20:
+            # d2 should be about 10x d1 but allow a wide margin for variability.
+            self.fail("HTTPHeaders.add() does not scale linearly: %s vs %s" % (d1, d2))
 
 class ParseRequestStartLineTest(unittest.TestCase):
     METHOD = "GET"
openSUSE Build Service is sponsored by