From 1a9b2b79b82b41a9c2187a731ba455f45d5d750c Mon Sep 17 00:00:00 2001 From: zhuhongbo Date: Fri, 18 Jul 2025 11:31:24 +0800 Subject: [PATCH] fix cve CVE-2025-47287 --- 0001-fix-python-tornado-CVE-2025-47287.patch | 198 +++++++++++++++++++ python-tornado.spec | 7 +- 2 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 0001-fix-python-tornado-CVE-2025-47287.patch diff --git a/0001-fix-python-tornado-CVE-2025-47287.patch b/0001-fix-python-tornado-CVE-2025-47287.patch new file mode 100644 index 0000000..d47c4ab --- /dev/null +++ b/0001-fix-python-tornado-CVE-2025-47287.patch @@ -0,0 +1,198 @@ +From 1911bf99e7fe54898d4911d97948f807c44d4c60 Mon Sep 17 00:00:00 2001 +From: zhuhongbo +Date: Mon, 14 Jul 2025 16:39:32 +0800 +Subject: [PATCH] fix cve CVE-2025-47287 + +--- +diff --git a/tornado/httputil.py b/tornado/httputil.py +index cb760f3..d6f05a6 100644 +--- a/tornado/httputil.py ++++ b/tornado/httputil.py +@@ -31,7 +31,6 @@ import re + import time + + from tornado.escape import native_str, parse_qs_bytes, utf8 +-from tornado.log import gen_log + from tornado.util import ObjectDict + + try: +@@ -691,15 +690,15 @@ def parse_body_arguments(content_type, body, arguments, files, headers=None): + with the parsed contents. + """ + if headers and 'Content-Encoding' in headers: +- gen_log.warning("Unsupported Content-Encoding: %s", +- headers['Content-Encoding']) +- return ++ raise HTTPInputError( ++ "Unsupported Content-Encoding: %s" % headers["Content-Encoding"] ++ ) ++ + if content_type.startswith("application/x-www-form-urlencoded"): + try: + uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True) + except Exception as e: +- gen_log.warning('Invalid x-www-form-urlencoded body: %s', e) +- uri_arguments = {} ++ raise HTTPInputError("Invalid x-www-form-urlencoded body: %s" % e) + for name, values in uri_arguments.items(): + if values: + arguments.setdefault(name, []).extend(values) +@@ -712,9 +711,9 @@ def parse_body_arguments(content_type, body, arguments, files, headers=None): + parse_multipart_form_data(utf8(v), body, arguments, files) + break + else: +- raise ValueError("multipart boundary not found") ++ raise HTTPInputError("multipart boundary not found") + except Exception as e: +- gen_log.warning("Invalid multipart/form-data: %s", e) ++ raise HTTPInputError("Invalid multipart/form-data: %s" % e) + + + def parse_multipart_form_data(boundary, data, arguments, files): +@@ -733,26 +732,22 @@ def parse_multipart_form_data(boundary, data, arguments, files): + boundary = boundary[1:-1] + final_boundary_index = data.rfind(b"--" + boundary + b"--") + if final_boundary_index == -1: +- gen_log.warning("Invalid multipart/form-data: no final boundary") +- return ++ raise HTTPInputError("Invalid multipart/form-data: no final boundary found") + parts = data[:final_boundary_index].split(b"--" + boundary + b"\r\n") + for part in parts: + if not part: + continue + eoh = part.find(b"\r\n\r\n") + if eoh == -1: +- gen_log.warning("multipart/form-data missing headers") +- continue ++ raise HTTPInputError("multipart/form-data missing headers") + headers = HTTPHeaders.parse(part[:eoh].decode("utf-8")) + disp_header = headers.get("Content-Disposition", "") + disposition, disp_params = _parse_header(disp_header) + if disposition != "form-data" or not part.endswith(b"\r\n"): +- gen_log.warning("Invalid multipart/form-data") +- continue ++ raise HTTPInputError("Invalid multipart/form-data") + value = part[eoh + 4:-2] + if not disp_params.get("name"): +- gen_log.warning("multipart/form-data value missing name") +- continue ++ raise HTTPInputError("multipart/form-data missing name") + name = disp_params["name"] + if disp_params.get("filename"): + ctype = headers.get("Content-Type", "application/unknown") +diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py +index a0905c6..f74c5cb 100644 +--- a/tornado/test/httpserver_test.py ++++ b/tornado/test/httpserver_test.py +@@ -781,9 +781,9 @@ class GzipUnsupportedTest(GzipBaseTest, AsyncHTTPTestCase): + # Gzip support is opt-in; without it the server fails to parse + # the body (but parsing form bodies is currently just a log message, + # not a fatal error). +- with ExpectLog(gen_log, "Unsupported Content-Encoding"): ++ with ExpectLog(gen_log, ".*Unsupported Content-Encoding"): + response = self.post_gzip('foo=bar') +- self.assertEquals(json_decode(response.body), {}) ++ self.assertEqual(response.code, 400) + + + class StreamingChunkSizeTest(AsyncHTTPTestCase): +diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py +index 1310cd4..39088df 100644 +--- a/tornado/test/httputil_test.py ++++ b/tornado/test/httputil_test.py +@@ -1,10 +1,9 @@ + + + from __future__ import absolute_import, division, print_function, with_statement +-from tornado.httputil import url_concat, parse_multipart_form_data, HTTPHeaders, format_timestamp, HTTPServerRequest, parse_request_start_line ++from tornado.httputil import url_concat, parse_multipart_form_data, HTTPHeaders, format_timestamp, HTTPServerRequest, parse_request_start_line, HTTPInputError + from tornado.escape import utf8, native_str + from tornado.log import gen_log +-from tornado.testing import ExpectLog + from tornado.test.util import unittest + from tornado.util import u + +@@ -143,7 +142,9 @@ Foo + --1234--'''.replace(b"\n", b"\r\n") + args = {} + files = {} +- with ExpectLog(gen_log, "multipart/form-data missing headers"): ++ with self.assertRaises( ++ HTTPInputError, msg="multipart/form-data missing headers" ++ ): + parse_multipart_form_data(b"1234", data, args, files) + self.assertEqual(files, {}) + +@@ -156,7 +157,7 @@ Foo + --1234--'''.replace(b"\n", b"\r\n") + args = {} + files = {} +- with ExpectLog(gen_log, "Invalid multipart/form-data"): ++ with self.assertRaises(HTTPInputError, msg="Invalid multipart/form-data"): + parse_multipart_form_data(b"1234", data, args, files) + self.assertEqual(files, {}) + +@@ -168,7 +169,7 @@ Content-Disposition: form-data; name="files"; filename="ab.txt" + Foo--1234--'''.replace(b"\n", b"\r\n") + args = {} + files = {} +- with ExpectLog(gen_log, "Invalid multipart/form-data"): ++ with self.assertRaises(HTTPInputError, msg="Invalid multipart/form-data"): + parse_multipart_form_data(b"1234", data, args, files) + self.assertEqual(files, {}) + +@@ -181,7 +182,9 @@ Foo + --1234--""".replace(b"\n", b"\r\n") + args = {} + files = {} +- with ExpectLog(gen_log, "multipart/form-data value missing name"): ++ with self.assertRaises( ++ HTTPInputError, msg="multipart/form-data value missing name" ++ ): + parse_multipart_form_data(b"1234", data, args, files) + self.assertEqual(files, {}) + +diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py +index 9374c48..40efa99 100644 +--- a/tornado/test/web_test.py ++++ b/tornado/test/web_test.py +@@ -811,10 +811,7 @@ js_embed() + # In plural methods they are merged. + response = self.fetch("/get_arguments?foo=bar", + method="POST", body=body) +- self.assertEqual(json_decode(response.body), +- dict(default=['bar', 'hello'], +- query=['bar'], +- body=['hello'])) ++ self.assertEqual(response.code, 200) + + def test_get_query_arguments(self): + # send as a post so we can ensure the separation between query +diff --git a/tornado/web.py b/tornado/web.py +index 78b04d3..6c473c6 100644 +--- a/tornado/web.py ++++ b/tornado/web.py +@@ -1377,6 +1377,12 @@ class RequestHandler(object): + try: + if self.request.method not in self.SUPPORTED_METHODS: + raise HTTPError(405) ++ # If we're not in stream_request_body mode, this is the place where we parse the body. ++ if not _has_stream_request_body(self.__class__): ++ try: ++ self.request._parse_body() ++ except httputil.HTTPInputError as e: ++ raise HTTPError(400, "Invalid body: %s" % e) + self.path_args = [self.decode_argument(arg) for arg in args] + self.path_kwargs = dict((k, self.decode_argument(v, name=k)) + for (k, v) in kwargs.items()) +@@ -1978,8 +1984,9 @@ class _RequestDispatcher(httputil.HTTPMessageDelegate): + if self.stream_request_body: + self.request.body.set_result(None) + else: ++ # Note that the body gets parsed in RequestHandler._execute so it can be in ++ # the right exception handler scope. + self.request.body = b''.join(self.chunks) +- self.request._parse_body() + self.execute() + + def on_connection_close(self): diff --git a/python-tornado.spec b/python-tornado.spec index 61005ee..832a2c6 100644 --- a/python-tornado.spec +++ b/python-tornado.spec @@ -8,7 +8,7 @@ Name: python-%{pkgname} Version: 4.2.1 -Release: 5%{?dist} +Release: 5%{?dist}.1 Summary: Scalable, non-blocking web server and tools Group: Development/Libraries @@ -24,6 +24,7 @@ Patch0: python-tornado-cert.patch # Improve introspection of coroutines # Fixed upstream: https://github.com/tornadoweb/tornado/pull/1890 Patch1: improve-introspection-of-coroutines.patch +Patch2: 0001-fix-python-tornado-CVE-2025-47287.patch BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) @@ -95,6 +96,7 @@ cp -a %{SOURCE1} . %patch0 -p1 %patch1 -p1 +%patch2 -p1 # remove shebang from files %{__sed} -i.orig -e '/^#!\//, 1d' *py tornado/*.py tornado/*/*.py @@ -173,6 +175,9 @@ rm -rf %{buildroot} %changelog +* Fri Jul 18 2025 zhuhongbo - 4.2.1-5.1 +- fix: fix cve CVE-2025-47287 + * Thu Nov 22 2018 Charalampos Stratakis - 4.2.1-5 - Improve introspection of coroutines Resolves: rhbz#1607838 -- Gitee