diff --git a/CVE-2025-47287-for-5.0.2.patch b/CVE-2025-47287-for-5.0.2.patch new file mode 100644 index 0000000000000000000000000000000000000000..439df2e521c4831ae102d2cdfcc4f84fdddbacc0 --- /dev/null +++ b/CVE-2025-47287-for-5.0.2.patch @@ -0,0 +1,192 @@ +From 5c90971b7c451be0ffbf57804c3d7093d50b30ff Mon Sep 17 00:00:00 2001 +From: xieyanlong +Date: Fri, 27 Jun 2025 15:26:28 +0800 +Subject: [PATCH] Raise errors instead of logging in multipart/form-data + parsing + +--- + tornado/httputil.py | 23 ++++++++--------------- + tornado/test/httpserver_test.py | 4 ++-- + tornado/test/httputil_test.py | 14 +++++++++----- + tornado/web.py | 11 ++++++++++- + 4 files changed, 29 insertions(+), 23 deletions(-) + +diff --git a/tornado/httputil.py b/tornado/httputil.py +index ceff735..19126ef 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, PY3 + + if PY3: +@@ -719,15 +718,13 @@ 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", ++ raise HTTPInputError("Unsupported Content-Encoding: %s" % + headers['Content-Encoding']) +- return + 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) +@@ -740,9 +737,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): +@@ -761,26 +758,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") + 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 value 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 a626369..68265c0 100644 +--- a/tornado/test/httpserver_test.py ++++ b/tornado/test/httpserver_test.py +@@ -857,9 +857,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 5c064dd..97f0002 100644 +--- a/tornado/test/httputil_test.py ++++ b/tornado/test/httputil_test.py +@@ -4,11 +4,11 @@ from __future__ import absolute_import, division, print_function + from tornado.httputil import ( + url_concat, parse_multipart_form_data, HTTPHeaders, format_timestamp, + HTTPServerRequest, parse_request_start_line, parse_cookie, qs_to_qsl, ++ HTTPInputError, + ) + from tornado.escape import utf8, native_str + from tornado.util import PY3 + from tornado.log import gen_log +-from tornado.testing import ExpectLog + from tornado.test.util import unittest + + import copy +@@ -197,7 +197,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, {}) + +@@ -210,7 +212,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, {}) + +@@ -222,7 +224,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, {}) + +@@ -235,7 +237,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/web.py b/tornado/web.py +index a1d2aa5..cd9d0f6 100644 +--- a/tornado/web.py ++++ b/tornado/web.py +@@ -1508,6 +1508,14 @@ class RequestHandler(object): + try: + if self.request.method not in self.SUPPORTED_METHODS: + raise HTTPError(405) ++ ++ # If we'r 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()) +@@ -2134,8 +2142,9 @@ class _HandlerDelegate(httputil.HTTPMessageDelegate): + if self.stream_request_body: + future_set_result_unless_cancelled(self.request.body, 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): +-- +2.45.2 + diff --git a/python-tornado.spec b/python-tornado.spec index 8033a5cc7201859fee1478cfcccfb52a667e16c3..a239a556faf581e594443fbb00f32b9c43c93349 100644 --- a/python-tornado.spec +++ b/python-tornado.spec @@ -1,6 +1,6 @@ Name: python-tornado Version: 5.0.2 -Release: 9 +Release: 10 Summary: a Python web framework and asynchronous networking library License: ASL 2.0 URL: http://www.tornadoweb.org @@ -8,6 +8,7 @@ Source0: https://files.pythonhosted.org/packages/source/t/tornado/torna Patch0: fix-erroneous-deprecation-warnings.patch Patch1: CVE-2023-28370.patch Patch2: CVE-2024-52804.patch +Patch3: CVE-2025-47287-for-5.0.2.patch BuildRequires: gcc python2-devel python2-singledispatch python3-devel @@ -64,6 +65,9 @@ and other applications that require a long-lived connection to each user. %{python3_sitearch}/* %changelog +* Fri Jun 27 2025 xieyanlong - 5.0.2-10 +- Fix CVE-2025-47287 + * Tue Nov 26 2024 liyajie - 5.0.2-9 - Fix CVE-2024-52804