summaryrefslogtreecommitdiffstats
path: root/meta-python/recipes-devtools/python/python3-tornado
diff options
context:
space:
mode:
Diffstat (limited to 'meta-python/recipes-devtools/python/python3-tornado')
-rw-r--r--meta-python/recipes-devtools/python/python3-tornado/CVE-2025-47287.patch232
1 files changed, 232 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-tornado/CVE-2025-47287.patch b/meta-python/recipes-devtools/python/python3-tornado/CVE-2025-47287.patch
new file mode 100644
index 0000000000..02439c43e0
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-tornado/CVE-2025-47287.patch
@@ -0,0 +1,232 @@
1From 85a6a33e774376ec5b286d3a4857c569b8a8c4a8 Mon Sep 17 00:00:00 2001
2From: Ben Darnell <ben@bendarnell.com>
3Date: Thu, 8 May 2025 13:29:43 -0400
4Subject: [PATCH] httputil: Raise errors instead of logging in
5 multipart/form-data parsing
6
7We used to continue after logging an error, which allowed repeated
8errors to spam the logs. The error raised here will still be logged,
9but only once per request, consistent with other error handling in
10Tornado.
11
12CVE: CVE-2025-47287
13Upstream-Status: Backport [https://github.com/tornadoweb/tornado/commit/cc61050e8f26697463142d99864b562e8470b41d]
14Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
15---
16 tornado/httputil.py | 30 +++++++++++-------------------
17 tornado/test/httpserver_test.py | 4 ++--
18 tornado/test/httputil_test.py | 13 ++++++++-----
19 tornado/web.py | 17 +++++++++++++----
20 4 files changed, 34 insertions(+), 30 deletions(-)
21
22diff --git a/tornado/httputil.py b/tornado/httputil.py
23index ebdc8059..090a977d 100644
24--- a/tornado/httputil.py
25+++ b/tornado/httputil.py
26@@ -34,7 +34,6 @@ import unicodedata
27 from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl
28
29 from tornado.escape import native_str, parse_qs_bytes, utf8
30-from tornado.log import gen_log
31 from tornado.util import ObjectDict, unicode_type
32
33
34@@ -762,25 +761,22 @@ def parse_body_arguments(
35 """
36 if content_type.startswith("application/x-www-form-urlencoded"):
37 if headers and "Content-Encoding" in headers:
38- gen_log.warning(
39- "Unsupported Content-Encoding: %s", headers["Content-Encoding"]
40+ raise HTTPInputError(
41+ "Unsupported Content-Encoding: %s" % headers["Content-Encoding"]
42 )
43- return
44 try:
45 # real charset decoding will happen in RequestHandler.decode_argument()
46 uri_arguments = parse_qs_bytes(body, keep_blank_values=True)
47 except Exception as e:
48- gen_log.warning("Invalid x-www-form-urlencoded body: %s", e)
49- uri_arguments = {}
50+ raise HTTPInputError("Invalid x-www-form-urlencoded body: %s" % e) from e
51 for name, values in uri_arguments.items():
52 if values:
53 arguments.setdefault(name, []).extend(values)
54 elif content_type.startswith("multipart/form-data"):
55 if headers and "Content-Encoding" in headers:
56- gen_log.warning(
57- "Unsupported Content-Encoding: %s", headers["Content-Encoding"]
58+ raise HTTPInputError(
59+ "Unsupported Content-Encoding: %s" % headers["Content-Encoding"]
60 )
61- return
62 try:
63 fields = content_type.split(";")
64 for field in fields:
65@@ -789,9 +785,9 @@ def parse_body_arguments(
66 parse_multipart_form_data(utf8(v), body, arguments, files)
67 break
68 else:
69- raise ValueError("multipart boundary not found")
70+ raise HTTPInputError("multipart boundary not found")
71 except Exception as e:
72- gen_log.warning("Invalid multipart/form-data: %s", e)
73+ raise HTTPInputError("Invalid multipart/form-data: %s" % e) from e
74
75
76 def parse_multipart_form_data(
77@@ -820,26 +816,22 @@ def parse_multipart_form_data(
78 boundary = boundary[1:-1]
79 final_boundary_index = data.rfind(b"--" + boundary + b"--")
80 if final_boundary_index == -1:
81- gen_log.warning("Invalid multipart/form-data: no final boundary")
82- return
83+ raise HTTPInputError("Invalid multipart/form-data: no final boundary found")
84 parts = data[:final_boundary_index].split(b"--" + boundary + b"\r\n")
85 for part in parts:
86 if not part:
87 continue
88 eoh = part.find(b"\r\n\r\n")
89 if eoh == -1:
90- gen_log.warning("multipart/form-data missing headers")
91- continue
92+ raise HTTPInputError("multipart/form-data missing headers")
93 headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"))
94 disp_header = headers.get("Content-Disposition", "")
95 disposition, disp_params = _parse_header(disp_header)
96 if disposition != "form-data" or not part.endswith(b"\r\n"):
97- gen_log.warning("Invalid multipart/form-data")
98- continue
99+ raise HTTPInputError("Invalid multipart/form-data")
100 value = part[eoh + 4 : -2]
101 if not disp_params.get("name"):
102- gen_log.warning("multipart/form-data value missing name")
103- continue
104+ raise HTTPInputError("multipart/form-data missing name")
105 name = disp_params["name"]
106 if disp_params.get("filename"):
107 ctype = headers.get("Content-Type", "application/unknown")
108diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py
109index 0b29a39c..5d5fb13a 100644
110--- a/tornado/test/httpserver_test.py
111+++ b/tornado/test/httpserver_test.py
112@@ -1131,9 +1131,9 @@ class GzipUnsupportedTest(GzipBaseTest, AsyncHTTPTestCase):
113 # Gzip support is opt-in; without it the server fails to parse
114 # the body (but parsing form bodies is currently just a log message,
115 # not a fatal error).
116- with ExpectLog(gen_log, "Unsupported Content-Encoding"):
117+ with ExpectLog(gen_log, ".*Unsupported Content-Encoding"):
118 response = self.post_gzip("foo=bar")
119- self.assertEqual(json_decode(response.body), {})
120+ self.assertEqual(response.code, 400)
121
122
123 class StreamingChunkSizeTest(AsyncHTTPTestCase):
124diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py
125index 975900aa..9494d0c1 100644
126--- a/tornado/test/httputil_test.py
127+++ b/tornado/test/httputil_test.py
128@@ -12,7 +12,6 @@ from tornado.httputil import (
129 )
130 from tornado.escape import utf8, native_str
131 from tornado.log import gen_log
132-from tornado.testing import ExpectLog
133 from tornado.test.util import ignore_deprecation
134
135 import copy
136@@ -195,7 +194,9 @@ Foo
137 b"\n", b"\r\n"
138 )
139 args, files = form_data_args()
140- with ExpectLog(gen_log, "multipart/form-data missing headers"):
141+ with self.assertRaises(
142+ HTTPInputError, msg="multipart/form-data missing headers"
143+ ):
144 parse_multipart_form_data(b"1234", data, args, files)
145 self.assertEqual(files, {})
146
147@@ -209,7 +210,7 @@ Foo
148 b"\n", b"\r\n"
149 )
150 args, files = form_data_args()
151- with ExpectLog(gen_log, "Invalid multipart/form-data"):
152+ with self.assertRaises(HTTPInputError, msg="Invalid multipart/form-data"):
153 parse_multipart_form_data(b"1234", data, args, files)
154 self.assertEqual(files, {})
155
156@@ -222,7 +223,7 @@ Foo--1234--""".replace(
157 b"\n", b"\r\n"
158 )
159 args, files = form_data_args()
160- with ExpectLog(gen_log, "Invalid multipart/form-data"):
161+ with self.assertRaises(HTTPInputError, msg="Invalid multipart/form-data"):
162 parse_multipart_form_data(b"1234", data, args, files)
163 self.assertEqual(files, {})
164
165@@ -236,7 +237,9 @@ Foo
166 b"\n", b"\r\n"
167 )
168 args, files = form_data_args()
169- with ExpectLog(gen_log, "multipart/form-data value missing name"):
170+ with self.assertRaises(
171+ HTTPInputError, msg="multipart/form-data value missing name"
172+ ):
173 parse_multipart_form_data(b"1234", data, args, files)
174 self.assertEqual(files, {})
175
176diff --git a/tornado/web.py b/tornado/web.py
177index 03939647..8ec5601b 100644
178--- a/tornado/web.py
179+++ b/tornado/web.py
180@@ -1751,6 +1751,14 @@ class RequestHandler(object):
181 try:
182 if self.request.method not in self.SUPPORTED_METHODS:
183 raise HTTPError(405)
184+
185+ # If we're not in stream_request_body mode, this is the place where we parse the body.
186+ if not _has_stream_request_body(self.__class__):
187+ try:
188+ self.request._parse_body()
189+ except httputil.HTTPInputError as e:
190+ raise HTTPError(400, "Invalid body: %s" % e) from e
191+
192 self.path_args = [self.decode_argument(arg) for arg in args]
193 self.path_kwargs = dict(
194 (k, self.decode_argument(v, name=k)) for (k, v) in kwargs.items()
195@@ -1941,7 +1949,7 @@ def _has_stream_request_body(cls: Type[RequestHandler]) -> bool:
196
197
198 def removeslash(
199- method: Callable[..., Optional[Awaitable[None]]]
200+ method: Callable[..., Optional[Awaitable[None]]],
201 ) -> Callable[..., Optional[Awaitable[None]]]:
202 """Use this decorator to remove trailing slashes from the request path.
203
204@@ -1970,7 +1978,7 @@ def removeslash(
205
206
207 def addslash(
208- method: Callable[..., Optional[Awaitable[None]]]
209+ method: Callable[..., Optional[Awaitable[None]]],
210 ) -> Callable[..., Optional[Awaitable[None]]]:
211 """Use this decorator to add a missing trailing slash to the request path.
212
213@@ -2394,8 +2402,9 @@ class _HandlerDelegate(httputil.HTTPMessageDelegate):
214 if self.stream_request_body:
215 future_set_result_unless_cancelled(self.request._body_future, None)
216 else:
217+ # Note that the body gets parsed in RequestHandler._execute so it can be in
218+ # the right exception handler scope.
219 self.request.body = b"".join(self.chunks)
220- self.request._parse_body()
221 self.execute()
222
223 def on_connection_close(self) -> None:
224@@ -3267,7 +3276,7 @@ class GZipContentEncoding(OutputTransform):
225
226
227 def authenticated(
228- method: Callable[..., Optional[Awaitable[None]]]
229+ method: Callable[..., Optional[Awaitable[None]]],
230 ) -> Callable[..., Optional[Awaitable[None]]]:
231 """Decorate methods with this to require that the user be logged in.
232