new file mode 100644
@@ -0,0 +1,149 @@
+From eba9e5652af3edbf9d8f2375194b5c63362d0c91 Mon Sep 17 00:00:00 2001
+From: Ben Darnell <ben@bendarnell.com>
+Date: Wed, 5 Aug 2026 21:20:58 -0400
+Subject: [PATCH] httputil: Enforce a new limit on the number of arguments in a
+ request
+
+Large POST bodies can be very expensive to parse in the worst case,
+so use the (new in Python 3.8) max_num_fields argument to limit the
+cost. A new field in ParseBodyConfig allows users to configure this
+limit. The default is 1000, which is the same as that used in php and
+node.js.
+
+(cherry picked from commit 8d6363ed7b69d5f0da806efe34d256627a2191de)
+
+CVE: CVE-2026-82397
+Upstream-Status: Backport [https://github.com/tornadoweb/tornado/commit/8d6363ed7b69d5f0da806efe34d256627a2191de]
+
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ tornado/escape.py | 16 ++++++++++++++--
+ tornado/httputil.py | 26 +++++++++++++++++++++++++-
+ tornado/test/httputil_test.py | 18 ++++++++++++++++++
+ 3 files changed, 57 insertions(+), 3 deletions(-)
+
+diff --git a/tornado/escape.py b/tornado/escape.py
+index 8515bf58..a1c16b36 100644
+--- a/tornado/escape.py
++++ b/tornado/escape.py
+@@ -171,7 +171,11 @@ def url_unescape(
+
+
+ def parse_qs_bytes(
+- qs: Union[str, bytes], keep_blank_values: bool = False, strict_parsing: bool = False
++ qs: Union[str, bytes],
++ keep_blank_values: bool = False,
++ strict_parsing: bool = False,
++ *,
++ max_num_fields: Optional[int] = None,
+ ) -> Dict[str, List[bytes]]:
+ """Parses a query string like urlparse.parse_qs,
+ but takes bytes and returns the values as byte strings.
+@@ -179,13 +183,21 @@ def parse_qs_bytes(
+ Keys still become type str (interpreted as latin1 in python3!)
+ because it's too painful to keep them as byte strings in
+ python3 and in practice they're nearly always ascii anyway.
++
++ .. versionadded:: 6.5.8
++ The ``max_num_fields`` argument. ValueError is raised if this limit is exceeded.
+ """
+ # This is gross, but python3 doesn't give us another way.
+ # Latin1 is the universal donor of character encodings.
+ if isinstance(qs, bytes):
+ qs = qs.decode("latin1")
+ result = urllib.parse.parse_qs(
+- qs, keep_blank_values, strict_parsing, encoding="latin1", errors="strict"
++ qs,
++ keep_blank_values,
++ strict_parsing,
++ encoding="latin1",
++ errors="strict",
++ max_num_fields=max_num_fields,
+ )
+ encoded = {}
+ for k, v in result.items():
+diff --git a/tornado/httputil.py b/tornado/httputil.py
+index f698db21..b80c796c 100644
+--- a/tornado/httputil.py
++++ b/tornado/httputil.py
+@@ -947,6 +947,23 @@ class ParseMultipartConfig:
+ """
+
+
++@dataclasses.dataclass
++class ParseUrlEncodedConfig:
++ """This class configures the parsing of ``application/x-www-form-urlencoded`` request bodies.
++
++ Its primary purpose is to place limits on the size and complexity of request messages
++ to avoid potential denial-of-service attacks.
++
++ .. versionadded:: 6.5.8
++ """
++
++ max_arguments: int = 1000
++ """The maximum number of arguments accepted in a urlencoded request.
++
++ Each ``<input>`` element in an HTML form corresponds to at least one argument.
++ """
++
++
+ @dataclasses.dataclass
+ class ParseBodyConfig:
+ """This class configures the parsing of request bodies.
+@@ -957,6 +974,9 @@ class ParseBodyConfig:
+ multipart: ParseMultipartConfig = dataclasses.field(
+ default_factory=ParseMultipartConfig
+ )
++ urlencoded: ParseUrlEncodedConfig = dataclasses.field(
++ default_factory=ParseUrlEncodedConfig
++ )
+ """Configuration for ``multipart/form-data`` request bodies."""
+
+
+@@ -1015,7 +1035,11 @@ def parse_body_arguments(
+ )
+ try:
+ # real charset decoding will happen in RequestHandler.decode_argument()
+- uri_arguments = parse_qs_bytes(body, keep_blank_values=True)
++ uri_arguments = parse_qs_bytes(
++ body,
++ keep_blank_values=True,
++ max_num_fields=config.urlencoded.max_arguments,
++ )
+ except Exception as e:
+ raise HTTPInputError("Invalid x-www-form-urlencoded body: %s" % e) from e
+ for name, values in uri_arguments.items():
+diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py
+index 92683ae9..afb15879 100644
+--- a/tornado/test/httputil_test.py
++++ b/tornado/test/httputil_test.py
+@@ -1,4 +1,5 @@
+ from tornado.httputil import (
++ parse_body_arguments,
+ url_concat,
+ parse_multipart_form_data,
+ HTTPHeaders,
+@@ -95,6 +96,23 @@ class QsParseTest(unittest.TestCase):
+ self.assertIn(("b", "2"), qsl)
+
+
++class UrlEncodedDataTest(unittest.TestCase):
++ def test_urlencoded_data(self):
++ data = b"a=1&b=2&a=3"
++ args, files = form_data_args()
++ parse_body_arguments("application/x-www-form-urlencoded", data, args, files)
++ self.assertEqual(args["a"], [b"1", b"3"])
++ self.assertEqual(args["b"], [b"2"])
++ self.assertEqual(files, {})
++
++ def test_max_arguments(self):
++ data = b"".join(b"a=1&" for _ in range(1001))
++ args, files = form_data_args()
++ with self.assertRaises(HTTPInputError) as cm:
++ parse_body_arguments("application/x-www-form-urlencoded", data, args, files)
++ self.assertIn("Max number of fields exceeded", str(cm.exception))
++
++
+ class MultipartFormDataTest(unittest.TestCase):
+ def test_file_upload(self):
+ data = b"""\
@@ -9,6 +9,7 @@ LIC_FILES_CHKSUM = "file://LICENSE;md5=3b83ef96387f14655fc854ddc3c6bd57"
SRC_URI[sha256sum] = "66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2"
SRC_URI += "file://CVE-2026-91990.patch \
+ file://CVE-2026-82397.patch \
"
inherit pypi python_setuptools_build_meta