17
0

2 Commits

Author SHA256 Message Date
b2373358e3 Accepting request 1323582 from devel:languages:python
OBS-URL: https://build.opensuse.org/request/show/1323582
OBS-URL: https://build.opensuse.org/package/show/openSUSE:Factory/python-tornado6?expand=0&rev=21
2025-12-20 20:45:03 +00:00
46a9d0e6f7 - Update to 6.5.4
* The in operator for HTTPHeaders was incorrectly case-sensitive, causing
    lookups to fail for headers with different casing than the original header
    name. This was a regression in version 6.5.3 and has been fixed to restore
    the intended case-insensitive behavior from version 6.5.2 and earlier.
- Update to 6.5.3 (bsc#1254903, bsc#1254905, bsc#1254904)
  * Fixed a denial-of-service vulnerability involving quadratic computation
    when parsing multipart/form-data request bodies. CVE-2025-67726
    Thanks to Finder16 for reporting this issue.
  * Fixed a denial-of-service vulnerability involving quadratic computation when
    parsing repeated HTTP headers. CVE-2025-67725.
    Thanks to Finder16 for reporting this issue.
  * Fixed a header injection and XSS vulnerability involving the reason argument
    to .RequestHandler.set_status and tornado.web.HTTPError. CVE-2025-67724.
    Thanks to Finder16 and Cheshire1225 for reporting this issue.
  * Several demo applications bundled with the Tornado repo (blog, chat,
    facebook) had an open redirect vulnerability which has been fixed. This is
    not covered by a CVE or security advisory since the demo applications are
    not included as a part of the Tornado package when installed, but developers
    who have copied code from these demos may which to review their own
    applications for open redirects.
    Thanks to J1vvoo for reporting this issue.
  * he s3server demo application contained some path traversal vulnerabilities.
    Since this demo application was not demonstrating any interesting aspects of
    Tornado, it has been deleted rather than being fixed.
    Thanks to J1vvoo for reporting this issue.
- Update to 6.5.2
  * Fixed a bug that resulted in WebSocket pings not being sent at the
    configured interval.
  * Improved logging for invalid Host headers. This was previously logged as an
    uncaught exception with a stack trace, now it is simply a 400 response
    (logged as a warning in the access log).
  * Restored the host argument to .HTTPServerRequest. This argument is
    deprecated and will be removed in the future, but its removal with no
    warning in 6.5.0 was a mistake.
  * Removed a debugging print statement that was left in the code.
  * Improved type hints for gen.multi.
- Update to 6.5.1
  * Fixed a bug in multipart/form-data parsing that could incorrectly reject
    filenames containing characters above U+00FF (i.e. most characters outside
    the Latin alphabet).

OBS-URL: https://build.opensuse.org/package/show/devel:languages:python/python-tornado6?expand=0&rev=48
2025-12-18 23:48:04 +00:00
8 changed files with 59 additions and 359 deletions

View File

@@ -1,113 +0,0 @@
From 9c163aebeaad9e6e7d28bac1f33580eb00b0e421 Mon Sep 17 00:00:00 2001
From: Ben Darnell <ben@bendarnell.com>
Date: Wed, 10 Dec 2025 15:15:25 -0500
Subject: [PATCH] web: Harden against invalid HTTP reason phrases
We allow applications to set custom reason phrases for the HTTP status
line (to support custom status codes), but if this were exposed to
untrusted data it could be exploited in various ways. This commit
guards against invalid reason phrases in both HTTP headers and in
error pages.
---
tornado/test/web_test.py | 15 ++++++++++++++-
tornado/web.py | 25 +++++++++++++++++++------
2 files changed, 33 insertions(+), 7 deletions(-)
Index: tornado-6.5/tornado/test/web_test.py
===================================================================
--- tornado-6.5.orig/tornado/test/web_test.py
+++ tornado-6.5/tornado/test/web_test.py
@@ -1746,7 +1746,7 @@ class StatusReasonTest(SimpleHandlerTest
class Handler(RequestHandler):
def get(self):
reason = self.request.arguments.get("reason", [])
- self.set_status(
+ raise HTTPError(
int(self.get_argument("code")),
reason=to_unicode(reason[0]) if reason else None,
)
@@ -1769,6 +1769,19 @@ class StatusReasonTest(SimpleHandlerTest
self.assertEqual(response.code, 682)
self.assertEqual(response.reason, "Unknown")
+ def test_header_injection(self):
+ response = self.fetch("/?code=200&reason=OK%0D%0AX-Injection:injected")
+ self.assertEqual(response.code, 200)
+ self.assertEqual(response.reason, "Unknown")
+ self.assertNotIn("X-Injection", response.headers)
+
+ def test_reason_xss(self):
+ response = self.fetch("/?code=400&reason=<script>alert(1)</script>")
+ self.assertEqual(response.code, 400)
+ self.assertEqual(response.reason, "Unknown")
+ self.assertNotIn(b"script", response.body)
+ self.assertIn(b"Unknown", response.body)
+
class DateHeaderTest(SimpleHandlerTestCase):
class Handler(RequestHandler):
Index: tornado-6.5/tornado/web.py
===================================================================
--- tornado-6.5.orig/tornado/web.py
+++ tornado-6.5/tornado/web.py
@@ -359,8 +359,10 @@ class RequestHandler:
:arg int status_code: Response status code.
:arg str reason: Human-readable reason phrase describing the status
- code. If ``None``, it will be filled in from
- `http.client.responses` or "Unknown".
+ code (for example, the "Not Found" in ``HTTP/1.1 404 Not Found``).
+ Normally determined automatically from `http.client.responses`; this
+ argument should only be used if you need to use a non-standard
+ status code.
.. versionchanged:: 5.0
@@ -369,6 +371,14 @@ class RequestHandler:
"""
self._status_code = status_code
if reason is not None:
+ if "<" in reason or not httputil._ABNF.reason_phrase.fullmatch(reason):
+ # Logically this would be better as an exception, but this method
+ # is called on error-handling paths that would need some refactoring
+ # to tolerate internal errors cleanly.
+ #
+ # The check for "<" is a defense-in-depth against XSS attacks (we also
+ # escape the reason when rendering error pages).
+ reason = "Unknown"
self._reason = escape.native_str(reason)
else:
self._reason = httputil.responses.get(status_code, "Unknown")
@@ -1345,7 +1355,8 @@ class RequestHandler:
reason = exception.reason
self.set_status(status_code, reason=reason)
try:
- self.write_error(status_code, **kwargs)
+ if status_code != 304:
+ self.write_error(status_code, **kwargs)
except Exception:
app_log.error("Uncaught exception in write_error", exc_info=True)
if not self._finished:
@@ -1373,7 +1384,7 @@ class RequestHandler:
self.finish(
"<html><title>%(code)d: %(message)s</title>"
"<body>%(code)d: %(message)s</body></html>"
- % {"code": status_code, "message": self._reason}
+ % {"code": status_code, "message": escape.xhtml_escape(self._reason)}
)
@property
@@ -2520,9 +2531,11 @@ class HTTPError(Exception):
mode). May contain ``%s``-style placeholders, which will be filled
in with remaining positional parameters.
:arg str reason: Keyword-only argument. The HTTP "reason" phrase
- to pass in the status line along with ``status_code``. Normally
+ to pass in the status line along with ``status_code`` (for example,
+ the "Not Found" in ``HTTP/1.1 404 Not Found``). Normally
determined automatically from ``status_code``, but can be used
- to use a non-standard numeric code.
+ to use a non-standard numeric code. This is not a general-purpose
+ error message.
"""
def __init__(

View File

@@ -1,124 +0,0 @@
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-6.5/tornado/httputil.py
===================================================================
--- tornado-6.5.orig/tornado/httputil.py
+++ tornado-6.5/tornado/httputil.py
@@ -183,8 +183,14 @@ class HTTPHeaders(StrMutableMapping):
pass
def __init__(self, *args: typing.Any, **kwargs: str) -> None: # noqa: F811
- self._dict = {} # type: typing.Dict[str, str]
- self._as_list = {} # type: typing.Dict[str, typing.List[str]]
+ # Formally, HTTP headers are a mapping from a field name to a "combined field value",
+ # which may be constructed from multiple field lines by joining them with commas.
+ # In practice, however, some headers (notably Set-Cookie) do not follow this convention,
+ # so we maintain a mapping from field name to a list of field lines in self._as_list.
+ # self._combined_cache is a cache of the combined field values derived from self._as_list
+ # 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 = None # type: Optional[str]
if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], HTTPHeaders):
# Copy constructor
@@ -207,9 +213,7 @@ class HTTPHeaders(StrMutableMapping):
norm_name = _normalize_header(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
@@ -266,7 +270,7 @@ class HTTPHeaders(StrMutableMapping):
if not _ABNF.field_value.fullmatch(new_part[1:]):
raise HTTPInputError("Invalid header continuation %r" % new_part)
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:
try:
name, value = line.split(":", 1)
@@ -305,22 +309,32 @@ class HTTPHeaders(StrMutableMapping):
def __setitem__(self, name: str, value: str) -> None:
norm_name = _normalize_header(name)
- self._dict[norm_name] = value
+ self._combined_cache[norm_name] = value
self._as_list[norm_name] = [value]
+ def __contains__(self, name: object) -> bool:
+ # 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: str) -> str:
- return self._dict[_normalize_header(name)]
+ header = _normalize_header(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: str) -> None:
norm_name = _normalize_header(name)
- del self._dict[norm_name]
+ del self._combined_cache[norm_name]
del self._as_list[norm_name]
def __len__(self) -> int:
- return len(self._dict)
+ return len(self._as_list)
def __iter__(self) -> Iterator[typing.Any]:
- return iter(self._dict)
+ return iter(self._as_list)
def copy(self) -> "HTTPHeaders":
# defined in dict but not in MutableMapping.
Index: tornado-6.5/tornado/test/httputil_test.py
===================================================================
--- tornado-6.5.orig/tornado/test/httputil_test.py
+++ tornado-6.5/tornado/test/httputil_test.py
@@ -534,6 +534,21 @@ class ParseRequestStartLineTest(unittest
self.assertEqual(parsed_start_line.path, self.PATH)
self.assertEqual(parsed_start_line.version, self.VERSION)
+ 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(10_000)
+ d2 = f(100_000)
+ if d2 / d1 > 20:
+ # d2 should be about 10x d1 but allow a wide margin for variability.
+ self.fail(f"HTTPHeaders.add() does not scale linearly: {d1=} vs {d2=}")
+
class ParseCookieTest(unittest.TestCase):
# These tests copied from Django:

View File

@@ -1,94 +0,0 @@
From 771472cfdaeebc0d89a9cc46e249f8891a6b29cd Mon Sep 17 00:00:00 2001
From: Ben Darnell <ben@bendarnell.com>
Date: Wed, 10 Dec 2025 10:55:02 -0500
Subject: [PATCH] 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
---
tornado/httputil.py | 29 ++++++++++++++++++++++-------
tornado/test/httputil_test.py | 23 +++++++++++++++++++++++
2 files changed, 45 insertions(+), 7 deletions(-)
Index: tornado-6.5/tornado/httputil.py
===================================================================
--- tornado-6.5.orig/tornado/httputil.py
+++ tornado-6.5/tornado/httputil.py
@@ -1062,19 +1062,34 @@ def parse_response_start_line(line: str)
# It has also been modified to support valueless parameters as seen in
# websocket extension negotiations, and to support non-ascii values in
# RFC 2231/5987 format.
+#
+# _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
+# RFCs for multipart/form-data) before making this change.
def _parseparam(s: str) -> Generator[str, None, None]:
- 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: str) -> Tuple[str, Dict[str, str]]:
Index: tornado-6.5/tornado/test/httputil_test.py
===================================================================
--- tornado-6.5.orig/tornado/test/httputil_test.py
+++ tornado-6.5/tornado/test/httputil_test.py
@@ -262,6 +262,29 @@ Foo
self.assertEqual(file["filename"], "ab.txt")
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):

View File

@@ -1,8 +1,8 @@
Index: tornado-6.5/tornado/util.py
Index: tornado-6.0.4/tornado/util.py
===================================================================
--- tornado-6.5.orig/tornado/util.py
+++ tornado-6.5/tornado/util.py
@@ -441,5 +441,7 @@ else:
--- tornado-6.0.4.orig/tornado/util.py 2020-03-11 11:42:49.610254636 +0100
+++ tornado-6.0.4/tornado/util.py 2020-03-11 11:43:51.470603323 +0100
@@ -468,5 +468,7 @@ else:
def doctests():
# type: () -> unittest.TestSuite
import doctest
@@ -10,11 +10,11 @@ Index: tornado-6.5/tornado/util.py
+ warnings.simplefilter("ignore", ResourceWarning)
return doctest.DocTestSuite()
Index: tornado-6.5/tornado/httputil.py
Index: tornado-6.0.4/tornado/httputil.py
===================================================================
--- tornado-6.5.orig/tornado/httputil.py
+++ tornado-6.5/tornado/httputil.py
@@ -1137,6 +1137,8 @@ def encode_username_password(
--- tornado-6.0.4.orig/tornado/httputil.py 2020-03-11 11:42:49.610254636 +0100
+++ tornado-6.0.4/tornado/httputil.py 2020-03-11 11:44:46.178911693 +0100
@@ -1032,6 +1032,8 @@ def encode_username_password(
def doctests():
# type: () -> unittest.TestSuite
import doctest
@@ -23,11 +23,11 @@ Index: tornado-6.5/tornado/httputil.py
return doctest.DocTestSuite()
Index: tornado-6.5/tornado/iostream.py
Index: tornado-6.0.4/tornado/iostream.py
===================================================================
--- tornado-6.5.orig/tornado/iostream.py
+++ tornado-6.5/tornado/iostream.py
@@ -1613,5 +1613,7 @@ class PipeIOStream(BaseIOStream):
--- tornado-6.0.4.orig/tornado/iostream.py 2020-03-11 11:42:49.610254636 +0100
+++ tornado-6.0.4/tornado/iostream.py 2020-03-11 11:45:31.015164413 +0100
@@ -1677,5 +1677,7 @@ class PipeIOStream(BaseIOStream):
def doctests() -> Any:
import doctest

View File

@@ -1,10 +1,47 @@
-------------------------------------------------------------------
Mon Dec 15 15:35:32 UTC 2025 - Nico Krapp <nico.krapp@suse.com>
Tue Dec 16 13:42:10 UTC 2025 - Nico Krapp <nico.krapp@suse.com>
- Add security patches:
* CVE-2025-67724.patch (bsc#1254903)
* CVE-2025-67725.patch (bsc#1254905)
* CVE-2025-67726.patch (bsc#1254904)
- Update to 6.5.4
* The in operator for HTTPHeaders was incorrectly case-sensitive, causing
lookups to fail for headers with different casing than the original header
name. This was a regression in version 6.5.3 and has been fixed to restore
the intended case-insensitive behavior from version 6.5.2 and earlier.
- Update to 6.5.3 (bsc#1254903, bsc#1254905, bsc#1254904)
* Fixed a denial-of-service vulnerability involving quadratic computation
when parsing multipart/form-data request bodies. CVE-2025-67726
Thanks to Finder16 for reporting this issue.
* Fixed a denial-of-service vulnerability involving quadratic computation when
parsing repeated HTTP headers. CVE-2025-67725.
Thanks to Finder16 for reporting this issue.
* Fixed a header injection and XSS vulnerability involving the reason argument
to .RequestHandler.set_status and tornado.web.HTTPError. CVE-2025-67724.
Thanks to Finder16 and Cheshire1225 for reporting this issue.
* Several demo applications bundled with the Tornado repo (blog, chat,
facebook) had an open redirect vulnerability which has been fixed. This is
not covered by a CVE or security advisory since the demo applications are
not included as a part of the Tornado package when installed, but developers
who have copied code from these demos may which to review their own
applications for open redirects.
Thanks to J1vvoo for reporting this issue.
* he s3server demo application contained some path traversal vulnerabilities.
Since this demo application was not demonstrating any interesting aspects of
Tornado, it has been deleted rather than being fixed.
Thanks to J1vvoo for reporting this issue.
- Update to 6.5.2
* Fixed a bug that resulted in WebSocket pings not being sent at the
configured interval.
* Improved logging for invalid Host headers. This was previously logged as an
uncaught exception with a stack trace, now it is simply a 400 response
(logged as a warning in the access log).
* Restored the host argument to .HTTPServerRequest. This argument is
deprecated and will be removed in the future, but its removal with no
warning in 6.5.0 was a mistake.
* Removed a debugging print statement that was left in the code.
* Improved type hints for gen.multi.
- Update to 6.5.1
* Fixed a bug in multipart/form-data parsing that could incorrectly reject
filenames containing characters above U+00FF (i.e. most characters outside
the Latin alphabet).
-------------------------------------------------------------------
Fri May 16 09:23:08 UTC 2025 - Daniel Garcia <daniel.garcia@suse.com>

View File

@@ -1,7 +1,7 @@
#
# spec file for package python-tornado6
#
# Copyright (c) 2025 SUSE LLC
# Copyright (c) 2025 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-tornado6
Version: 6.5
Version: 6.5.4
Release: 0
Summary: Open source version of scalable, non-blocking web server that power FriendFeed
License: Apache-2.0
@@ -27,12 +27,6 @@ Source: https://files.pythonhosted.org/packages/source/t/tornado/tornado
Source99: python-tornado6-rpmlintrc
# PATCH-FIX-OPENSUSE ignore-resourcewarning-doctests.patch -- ignore resource warnings on OBS
Patch0: ignore-resourcewarning-doctests.patch
# PATCH-FIX-UPSTREAM CVE-2025-67724.patch bsc#1254903
Patch1: CVE-2025-67724.patch
# PATCH-FIX-UPSTREAM CVE-2025-67725.patch bsc#1254905
Patch2: CVE-2025-67725.patch
# PATCH-FIX-UPSTREAM CVE-2025-67726.patch bsc#1254904
Patch3: CVE-2025-67726.patch
BuildRequires: %{python_module base >= 3.8}
BuildRequires: %{python_module devel}
BuildRequires: %{python_module pip}

3
tornado-6.5.4.tar.gz Normal file
View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7
size 513632

Binary file not shown.