From bf1ca462d037c6cb8c2c52bf2e28499dafa58d24 Mon Sep 17 00:00:00 2001 From: Yogita Urade Date: Wed, 4 Nov 2026 23:08:18 -0800 Subject: [PATCH] python3-tornado: Fix CVE-2026-116577 This patch applies the upstream fix as referenced in [4], using the commit shown in [1], [2] and [3]. [1] https://github.com/tornadoweb/tornado/commit/1b7a1dc9f340f52de929852e8125aa12f25297c9 [2] https://github.com/tornadoweb/tornado/commit/09591f65286a600c2196df9c33b792b889a6bccb [3] https://github.com/tornadoweb/tornado/commit/3ef5ec3fc7a158ecd0ca7bfc5fcee42fa2773094 [4] https://github.com/tornadoweb/tornado/pull/3765 [5] https://nvd.nist.gov/vuln/detail/CVE-2026-116577 Signed-off-by: Yogita Urade Signed-off-by: Anuj Mittal --- .../python3-tornado/CVE-2026-116577_p1.patch | 195 ++++++++++++++++++ .../python3-tornado/CVE-2026-116577_p2.patch | 99 +++++++++ .../python3-tornado/CVE-2026-116577_p3.patch | 101 +++++++++ .../python/python3-tornado_6.5.7.bb | 3 + 4 files changed, 398 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p1.patch create mode 100644 meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p2.patch create mode 100644 meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p3.patch diff --git a/meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p1.patch b/meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p1.patch new file mode 100644 index 0000000000..0c4db00069 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p1.patch @@ -0,0 +1,195 @@ +From 1b7a1dc9f340f52de929852e8125aa12f25297c9 Mon Sep 17 00:00:00 2001 +From: Ben Darnell +Date: Tue, 6 Oct 2026 09:59:02 -0400 +Subject: [PATCH] httputil: Remove support for "obsolete line folding" + +This optional part of the HTTP specification is no longer used +in practice, and continuing to support it adds complexity and risk +of DoS via quadratic performance on malicious data. + +CVE: CVE-2026-116577 +Upstream-Status: Backport [https://github.com/tornadoweb/tornado/commit/1b7a1dc9f340f52de929852e8125aa12f25297c9] + +Backport Changes: +- Kept unittest.TestCase for HTTPHeadersTest because 6.5.7's httputil_test.py + does not import tornado.test.util.TestCase. +- Updated test_multi_line_headers to preserve folded-header acceptance for Curl + and expect a 599 response from simple_httpclient. +- The httputil_test.py hunk removes two extra target-version lines because + 6.5.7 formats the removed multiline .replace() expression across three lines. + +Signed-off-by: Yogita Urade +--- + tornado/httputil.py | 34 +++++++++--------------- + tornado/test/httpclient_test.py | 17 +++++++++++-- + tornado/test/httputil_test.py | 47 +++------------------------------ + 3 files changed, 29 insertions(+), 69 deletions(-) + +diff --git a/tornado/httputil.py b/tornado/httputil.py +index 6a5db233..45e1883d 100644 +--- a/tornado/httputil.py ++++ b/tornado/httputil.py +@@ -195,7 +195,6 @@ class HTTPHeaders(StrMutableMapping): + # 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 + for k, v in args[0].get_all(): +@@ -219,7 +218,6 @@ class HTTPHeaders(StrMutableMapping): + if _FORBIDDEN_HEADER_CHARS_RE.search(value): + raise HTTPInputError("Invalid header value %r" % value) + norm_name = _normalize_header(name) +- self._last_key = norm_name + if norm_name in self: + self._combined_cache.pop(norm_name, None) + self._as_list[norm_name].append(value) +@@ -260,6 +258,11 @@ class HTTPHeaders(StrMutableMapping): + In Tornado 7.0, certain deprecated features of HTTP will become errors. + Specifically, line folding and the use of LF (with CR) as a line separator + will be removed. ++ ++ .. versionchanged:: 6.6 ++ Support for line folding was removed due to security/DoS concerns. This removal ++ was ahead of the original removal schedule announced for Tornado 7.0. ++ Support for LF without CR remains until Tornado 7.0. + """ + if m := re.search(r"\r?\n$", line): + # RFC 9112 section 2.2: a recipient MAY recognize a single LF as a line +@@ -270,27 +273,14 @@ class HTTPHeaders(StrMutableMapping): + # Empty line, or the final CRLF of a header block. + return + if line[0] in HTTP_WHITESPACE: +- # continuation of a multi-line header +- # TODO(7.0): Remove support for line folding. +- if self._last_key is None: +- raise HTTPInputError("first header line cannot start with whitespace") +- new_part = " " + line.strip(HTTP_WHITESPACE) +- if _chars_are_bytes: +- if not _ABNF.field_value.fullmatch(new_part[1:]): +- raise HTTPInputError("Invalid header continuation %r" % new_part) +- else: +- if _FORBIDDEN_HEADER_CHARS_RE.search(new_part): +- raise HTTPInputError("Invalid header value %r" % new_part) +- self._as_list[self._last_key][-1] += new_part +- self._combined_cache.pop(self._last_key, None) +- else: +- try: +- name, value = line.split(":", 1) +- except ValueError: +- raise HTTPInputError("no colon in header line") +- self.add( +- name, value.strip(HTTP_WHITESPACE), _chars_are_bytes=_chars_are_bytes ++ raise HTTPInputError( ++ "header line cannot start with whitespace, folding not supported" + ) ++ try: ++ name, value = line.split(":", 1) ++ except ValueError: ++ raise HTTPInputError("no colon in header line") ++ self.add(name, value.strip(HTTP_WHITESPACE), _chars_are_bytes=_chars_are_bytes) + + @classmethod + def parse(cls, headers: str, *, _chars_are_bytes: bool = True) -> "HTTPHeaders": +diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py +index 46300200..4cb2e850 100644 +--- a/tornado/test/httpclient_test.py ++++ b/tornado/test/httpclient_test.py +@@ -734,10 +734,14 @@ def test_multi_line_headers(self): + % (resp.body, value, container), + ) + + def test_multi_line_headers(self): +- # Multi-line http headers are rare but rfc-allowed +- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 ++ # Line folding is an obsolete way of representing headers split across multiple lines. ++ # https://www.rfc-editor.org/info/rfc9112/#section-5.2 ++ # ++ # They must either be folded into a single line or rejected. ++ # Tornado 6.6 removed support for line folding in simple_httpclient, but they are ++ # still supported in libcurl. + sock, port = bind_unused_port() + with closing(sock): + + @gen.coroutine +@@ -760,10 +764,15 @@ def test_multi_line_headers(self): + + netutil.add_accept_handler(sock, accept_callback) # type: ignore + try: + resp = self.fetch("http://127.0.0.1:%d/" % port) +- resp.rethrow() +- self.assertEqual(resp.headers["X-XSS-Protection"], "1; mode=block") ++ if self.http_client.__class__.__module__ == "tornado.curl_httpclient": ++ resp.rethrow() ++ self.assertEqual( ++ resp.headers["X-XSS-Protection"], "1; mode=block" ++ ) ++ else: ++ self.assertEqual(resp.code, 599) + finally: + self.io_loop.remove_handler(sock.fileno()) + + @gen_test +diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py +index 0e9d5c41..40108d9c 100644 +--- a/tornado/test/httputil_test.py ++++ b/tornado/test/httputil_test.py +@@ -384,50 +384,11 @@ Content-Disposition: form-data; name="files"; filename="ab.txt" + + + class HTTPHeadersTest(unittest.TestCase): +- def test_multi_line(self): +- # Lines beginning with whitespace are appended to the previous line +- # with any leading whitespace replaced by a single space. +- # Note that while multi-line headers are a part of the HTTP spec, +- # their use is strongly discouraged. +- data = """\ +-Foo: bar +- baz +-Asdf: qwer +-\tzxcv +-Foo: even +- more +- lines +-""".replace( +- "\n", "\r\n" +- ) +- headers = HTTPHeaders.parse(data) +- self.assertEqual(headers["asdf"], "qwer zxcv") +- self.assertEqual(headers.get_list("asdf"), ["qwer zxcv"]) +- self.assertEqual(headers["Foo"], "bar baz,even more lines") +- self.assertEqual(headers.get_list("foo"), ["bar baz", "even more lines"]) +- self.assertEqual( +- sorted(list(headers.get_all())), +- [("Asdf", "qwer zxcv"), ("Foo", "bar baz"), ("Foo", "even more lines")], +- ) +- # Verify case insensitivity in-operator +- self.assertTrue("asdf" in headers) +- self.assertTrue("Asdf" in headers) +- +- def test_continuation(self): ++ def test_folding_rejected(self): + data = "Foo: bar\r\n\tasdf" +- headers = HTTPHeaders.parse(data) +- self.assertEqual(headers["Foo"], "bar asdf") +- +- # If the first line starts with whitespace, it's a +- # continuation line with nothing to continue, so reject it +- # (with a proper error). +- data = " Foo: bar" +- self.assertRaises(HTTPInputError, HTTPHeaders.parse, data) +- +- # \f (formfeed) is whitespace according to str.isspace, but +- # not according to the HTTP spec. +- data = "Foo: bar\r\n\fasdf" +- self.assertRaises(HTTPInputError, HTTPHeaders.parse, data) ++ with self.assertRaises(HTTPInputError): ++ # Folded continuation lines are not supported. ++ HTTPHeaders.parse(data) + + def test_forbidden_ascii_characters(self): + # Control characters and ASCII whitespace other than space, tab, and CRLF are not allowed in +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p2.patch b/meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p2.patch new file mode 100644 index 0000000000..0d1a7e5732 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p2.patch @@ -0,0 +1,99 @@ +From 09591f65286a600c2196df9c33b792b889a6bccb Mon Sep 17 00:00:00 2001 +From: Claude +Date: Tue, 6 Oct 2026 14:14:19 +0000 +Subject: [PATCH] http1connection: Notify client delegate of header parse + errors + +Previously, an HTTPInputError raised while parsing response headers +happened before the delegate was marked as needing on_connection_close, +and close() cleared the stream's close callback, so simple_httpclient +never completed the request and it hung until the timeout. Now the +delegate is told the connection closed, matching the behavior for +errors raised after headers_received. + +Update test_multi_line_headers to expect an error from simple_httpclient +now that line folding is no longer supported (curl still accepts it). + +CVE: CVE-2026-116577 +Upstream-Status: Backport [https://github.com/tornadoweb/tornado/commit/09591f65286a600c2196df9c33b792b889a6bccb] + +Backport Changes: +- Added a supports_line_folding flag to the shared test; Curl keeps the + acceptance check while Simple expects HTTPError for folded response headers. + +Co-Authored-By: Claude Opus 5.5 +Claude-Session: https://claude.ai/code/session_016cNR4NyeuzQ1qFdqCk1yum +Signed-off-by: Yogita Urade +--- + tornado/http1connection.py | 5 +++++ + tornado/test/httpclient_test.py | 19 +++++++++++++------ + tornado/test/curl_httpclient_test.py | 2 ++ + 3 files changed, 20 insertions(+), 6 deletions(-) + +diff --git a/tornado/http1connection.py b/tornado/http1connection.py +--- a/tornado/http1connection.py ++++ b/tornado/http1connection.py +@@ -316,6 +316,11 @@ async def _read_message + gen_log.info("Malformed HTTP message from %s: %s", self.context, e) + if not self.is_client: + await self.stream.write(b"HTTP/1.1 400 Bad Request\r\n\r\n") ++ elif not need_delegate_close: ++ # The error came before headers_received, so the delegate ++ # doesn't know about this message yet. Tell it the ++ # connection is closed so a client request doesn't hang. ++ need_delegate_close = True + self.close() + return False + finally: + +diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py +--- a/tornado/test/httpclient_test.py ++++ b/tornado/test/httpclient_test.py +@@ -263,6 +263,9 @@ class HTTPClientCommonTestCase + + + class HTTPClientCommonTestCase(AsyncHTTPTestCase): ++ # Set by subclasses whose client accepts obsolete line folding in ++ # response headers (see test_multi_line_headers). ++ supports_line_folding = False + # Set by subclasses whose client decodes ``Content-Encoding: br``. No + # implementation asks for brotli, so this is only true where the client + # decodes an encoding it did not request (see BrotliBombHandler). +@@ -764,14 +765,18 @@ class HTTPClientCommonTestCase + + netutil.add_accept_handler(sock, accept_callback) # type: ignore + try: +- resp = self.fetch("http://127.0.0.1:%d/" % port) +- if self.http_client.__class__.__module__ == "tornado.curl_httpclient": ++ if self.supports_line_folding: ++ resp = self.fetch("http://127.0.0.1:%d/" % port) + resp.rethrow() +- self.assertEqual( +- resp.headers["X-XSS-Protection"], "1; mode=block" +- ) ++ self.assertEqual(resp.headers["X-XSS-Protection"], "1; mode=block") + else: +- self.assertEqual(resp.code, 599) ++ with ExpectLog( ++ gen_log, "Malformed HTTP message.*folding", level=logging.INFO ++ ): ++ with self.assertRaises(HTTPError): ++ self.fetch( ++ "http://127.0.0.1:%d/" % port, raise_error=True ++ ) + finally: + self.io_loop.remove_handler(sock.fileno()) + + +diff --git a/tornado/test/curl_httpclient_test.py b/tornado/test/curl_httpclient_test.py +--- a/tornado/test/curl_httpclient_test.py ++++ b/tornado/test/curl_httpclient_test.py +@@ -45,6 +45,8 @@ class CurlHTTPClientCommonTestCase + # not we listed it in Accept-Encoding, so brotli is reachable whenever + # libcurl was built with it (which is common). + decompresses_brotli = pycurl is not None and "brotli" in pycurl.version ++ # libcurl still accepts obsolete line folding in response headers. ++ supports_line_folding = True + + def get_http_client(self): + client = CurlAsyncHTTPClient(defaults=dict(allow_ipv6=False)) diff --git a/meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p3.patch b/meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p3.patch new file mode 100644 index 0000000000..33670c88b5 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-tornado/CVE-2026-116577_p3.patch @@ -0,0 +1,101 @@ +From 3ef5ec3fc7a158ecd0ca7bfc5fcee42fa2773094 Mon Sep 17 00:00:00 2001 +From: Claude +Date: Tue, 6 Oct 2026 15:28:59 +0000 +Subject: [PATCH] test: Accept either outcome in test_multi_line_headers + +Rather than declaring per-implementation whether line folding is +supported (which was wrong when --httpclient changes the default +client), accept either a correctly unfolded header or a 599 error. + +CVE: CVE-2026-116577 +Upstream-Status: Backport [https://github.com/tornadoweb/tornado/commit/3ef5ec3fc7a158ecd0ca7bfc5fcee42fa2773094] + +Backport Changes: +- Rebased the test update on p2's 6.5.7-specific test body. Its fetch call and + error assertion use a different line layout, accounting for two additional + deleted lines in httpclient_test.py compared with upstream. + +Co-Authored-By: Claude Opus 5.5 +Claude-Session: https://claude.ai/code/session_016cNR4NyeuzQ1qFdqCk1yum +Signed-off-by: Yogita Urade +--- + tornado/test/httpclient_test.py | 37 +++++++++++++++++----------------- + tornado/test/curl_httpclient_test.py | 2 -- + 2 files changed, 19 insertions(+), 20 deletions(-) + +diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py +index bb7de5c4..449f35a4 100644 +--- a/tornado/test/httpclient_test.py ++++ b/tornado/test/httpclient_test.py +@@ -263,9 +263,6 @@ class HTTPClientCommonTestCase(AsyncHTTPTestCase): + + + class HTTPClientCommonTestCase(AsyncHTTPTestCase): +- # Set by subclasses whose client accepts obsolete line folding in +- # response headers (see test_multi_line_headers). +- supports_line_folding = False + # Set by subclasses whose client decodes ``Content-Encoding: br``. No + # implementation asks for brotli, so this is only true where the client + # decodes an encoding it did not request (see BrotliBombHandler). +@@ -741,9 +738,10 @@ def test_multi_line_headers(self): + # Line folding is an obsolete way of representing headers split across multiple lines. + # https://www.rfc-editor.org/info/rfc9112/#section-5.2 + # +- # They must either be folded into a single line or rejected. +- # Tornado 6.6 removed support for line folding in simple_httpclient, but they are +- # still supported in libcurl. ++ # The spec requires user agents to unfold them, but simple_httpclient ++ # rejects them as of Tornado 6.6 while libcurl still accepts them. Either ++ # outcome is acceptable here; what matters is that the client doesn't hang ++ # and doesn't misparse the header. + sock, port = bind_unused_port() + with closing(sock): + +@@ -767,18 +765,21 @@ def test_multi_line_headers(self): + + netutil.add_accept_handler(sock, accept_callback) # type: ignore + try: +- if self.supports_line_folding: +- resp = self.fetch("http://127.0.0.1:%d/" % port) +- resp.rethrow() +- self.assertEqual(resp.headers["X-XSS-Protection"], "1; mode=block") +- else: +- with ExpectLog( +- gen_log, "Malformed HTTP message.*folding", level=logging.INFO +- ): +- with self.assertRaises(HTTPError): +- self.fetch( +- "http://127.0.0.1:%d/" % port, raise_error=True +- ) ++ with ExpectLog( ++ gen_log, ++ "Malformed HTTP message.*folding", ++ level=logging.INFO, ++ required=False, ++ ): ++ try: ++ resp = self.fetch("http://127.0.0.1:%d/" % port) ++ except HTTPError as e: ++ self.assertEqual(e.code, 599) ++ else: ++ resp.rethrow() ++ self.assertEqual( ++ resp.headers["X-XSS-Protection"], "1; mode=block" ++ ) + finally: + self.io_loop.remove_handler(sock.fileno()) + + +diff --git a/tornado/test/curl_httpclient_test.py b/tornado/test/curl_httpclient_test.py +index 7a53f206..d7eb1adf 100644 +--- a/tornado/test/curl_httpclient_test.py ++++ b/tornado/test/curl_httpclient_test.py +@@ -45,8 +45,6 @@ class CurlHTTPClientCommonTestCase + # not we listed it in Accept-Encoding, so brotli is reachable whenever + # libcurl was built with it (which is common). + decompresses_brotli = pycurl is not None and "brotli" in pycurl.version +- # libcurl still accepts obsolete line folding in response headers. +- supports_line_folding = True + + def get_http_client(self): + client = CurlAsyncHTTPClient(defaults=dict(allow_ipv6=False)) diff --git a/meta-python/recipes-devtools/python/python3-tornado_6.5.7.bb b/meta-python/recipes-devtools/python/python3-tornado_6.5.7.bb index 1dd2ff3c32..87b6721320 100644 --- a/meta-python/recipes-devtools/python/python3-tornado_6.5.7.bb +++ b/meta-python/recipes-devtools/python/python3-tornado_6.5.7.bb @@ -16,6 +16,9 @@ SRC_URI += "file://CVE-2026-91990.patch \ file://CVE-2026-103263.patch \ file://CVE-2026-103263-dependent.patch \ file://CVE-2026-115927.patch \ + file://CVE-2026-116577_p1.patch \ + file://CVE-2026-116577_p2.patch \ + file://CVE-2026-116577_p3.patch \ " inherit pypi python_setuptools_build_meta