File fixes-for-security-issues-cve-2025-13836-cve-2025-67.patch of Package venv-salt-minion

From 324c7740438fd0bbcde1e0b6be70c92007c022ac Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <vzhestkov@suse.com>
Date: Wed, 14 Jan 2026 14:08:48 +0100
Subject: [PATCH] Fixes for security issues (CVE-2025-13836,
 CVE-2025-67725, CVE-2025-67726) (#744)

* Fixes for security issues (CVE-2025-67725)

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.

* Patch tornado for (BDSA-2025-60811, CVE-2025-67726)

httputil: Fix quadratic behavior in _parseparam

Prior to this change, _parseparam had O(n^2) behavior when parsing
certain inputs, which could be a DoS vector. This change adapts
logic from the equivalent function in the python standard library
in https://github.com/python/cpython/pull/136072/files

* Set a safe limit to http.client response read (CVE-2025-13836)

https://github.com/saltstack/salt/pull/68611

* Remove duplicated test

---------

Co-authored-by: Twangboy <shane.d.lee@gmail.com>
Co-authored-by: Marek Czernek <marek.czernek@suse.com>
---
 salt/ext/tornado/httputil.py           | 56 ++++++++++++++++++--------
 salt/ext/tornado/test/httputil_test.py | 38 +++++++++++++++++
 salt/utils/nxos.py                     |  3 +-
 3 files changed, 79 insertions(+), 18 deletions(-)

diff --git a/salt/ext/tornado/httputil.py b/salt/ext/tornado/httputil.py
index 4866b0c991..78953c5f6b 100644
--- a/salt/ext/tornado/httputil.py
+++ b/salt/ext/tornado/httputil.py
@@ -139,8 +139,8 @@ class HTTPHeaders(MutableMapping):
     """
 
     def __init__(self, *args, **kwargs):
-        self._dict = {}  # type: typing.Dict[str, str]
         self._as_list = {}  # type: typing.Dict[str, typing.List[str]]
+        self._combined_cache = {} # type: typing.Dict[str, str]
         self._last_key = None
         if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], HTTPHeaders):
             # Copy constructor
@@ -158,9 +158,7 @@ class HTTPHeaders(MutableMapping):
         norm_name = _normalized_headers[name]
         self._last_key = norm_name
         if norm_name in self:
-            self._dict[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
@@ -193,7 +191,7 @@ class HTTPHeaders(MutableMapping):
             # continuation of a multi-line header
             new_part = " " + line.lstrip(HTTP_WHITESPACE)
             self._as_list[self._last_key][-1] += new_part
-            self._dict[self._last_key] += new_part
+            self._combined_cache.pop(self._last_key, None)
         else:
             name, value = line.split(":", 1)
             self.add(name, value.strip(HTTP_WHITESPACE))
@@ -216,23 +214,33 @@ class HTTPHeaders(MutableMapping):
 
     def __setitem__(self, name, value):
         norm_name = _normalized_headers[name]
-        self._dict[norm_name] = value
+        self._combined_cache[norm_name] = value
         self._as_list[norm_name] = [value]
 
+    def __contains__(self, name):
+        # This is an important optimization to avoid the expensive concatenation
+        # in __getitem__ when it's not needed.
+        if not isinstance(name, str):
+            return False
+        return name in self._as_list
+
     def __getitem__(self, name):
         # type: (str) -> str
-        return self._dict[_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]
-        del self._dict[norm_name]
+        del self._combined_cache[norm_name]
         del self._as_list[norm_name]
 
     def __len__(self):
-        return len(self._dict)
+        return len(self._as_list)
 
     def __iter__(self):
-        return iter(self._dict)
+        return iter(self._as_list)
 
     def copy(self):
         # defined in dict but not in MutableMapping.
@@ -894,19 +902,33 @@ def parse_response_start_line(line):
 # combinations of semicolons and double quotes.
 # It has also been modified to support valueless parameters as seen in
 # websocket extension negotiations.
+#
+# _parseparam has been further modified with the logic from
+# https://github.com/python/cpython/pull/136072/files
+# to avoid quadratic behavior when parsing semicolons in quoted strings.
+#
+# TODO: See if we can switch to email.message.Message for this functionality.
+# This is the suggested replacement for the cgi.py module now that cgi has
+# been removed from recent versions of Python.  We need to verify that
+# the email module is consistent with our existing behavior (and all relevant
 
 
 def _parseparam(s):
-    while s[:1] == ";":
-        s = s[1:]
-        end = s.find(";")
-        while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
-            end = s.find(";", end + 1)
+    start = 0
+    while s.find(";", start) == start:
+        start += 1
+        end = s.find(";", start)
+        ind, diff = start, 0
+        while end > 0:
+            diff += s.count('"', ind, end) - s.count('\\"', ind, end)
+            if diff % 2 == 0:
+                break
+            end, ind = ind, s.find(";", end + 1)
         if end < 0:
             end = len(s)
-        f = s[:end]
+        f = s[start:end]
         yield f.strip()
-        s = s[end:]
+        start = end
 
 
 def _parse_header(line):
diff --git a/salt/ext/tornado/test/httputil_test.py b/salt/ext/tornado/test/httputil_test.py
index c613b4e41a..bdbfaa2b6a 100644
--- a/salt/ext/tornado/test/httputil_test.py
+++ b/salt/ext/tornado/test/httputil_test.py
@@ -245,6 +245,30 @@ Foo
         self.assertEqual(file["body"], b"Foo")
 
 
+    def test_disposition_param_linear_performance(self):
+        # This is a regression test for performance of parsing parameters
+        # to the content-disposition header, specifically for semicolons within
+        # quoted strings.
+        def f(n):
+            start = time.time()
+            message = (
+                b"--1234\r\nContent-Disposition: form-data; "
+                + b'x="'
+                + b";" * n
+                + b'"; '
+                + b'name="files"; filename="a.txt"\r\n\r\nFoo\r\n--1234--\r\n'
+            )
+            args: dict[str, list[bytes]] = {}
+            files: dict[str, list[HTTPFile]] = {}
+            parse_multipart_form_data(b"1234", message, args, files)
+            return time.time() - start
+
+        d1 = f(1_000)
+        d2 = f(10_000)
+        if d2 / d1 > 20:
+            self.fail(f"Disposition param parsing is not linear: {d1=} vs {d2=}")
+
+
 class HTTPHeadersTest(unittest.TestCase):
     def test_multi_line(self):
         # Lines beginning with whitespace are appended to the previous line
@@ -367,6 +391,20 @@ Foo: even
         headers2 = HTTPHeaders.parse(str(headers))
         self.assertEquals(headers, headers2)
 
+    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):
     # Make sure that all the input types are supported.
diff --git a/salt/utils/nxos.py b/salt/utils/nxos.py
index 2572a76267..654290155e 100644
--- a/salt/utils/nxos.py
+++ b/salt/utils/nxos.py
@@ -212,7 +212,8 @@ class NxapiClient:
             body = response
 
         if self.nxargs["connect_over_uds"]:
-            body = json.loads(response.read().decode("utf-8"))
+            max_safe_read = 10 * 1024 * 1024
+            body = json.loads(response.read(max_safe_read).decode("utf-8"))
 
         # Proceed with caution.  The JSON may not be complete.
         # Don't just return body['ins_api']['outputs']['output'] directly.
-- 
2.52.0

openSUSE Build Service is sponsored by