Index: tornado-6.4/tornado/escape.py
===================================================================
--- tornado-6.4.orig/tornado/escape.py
+++ tornado-6.4/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():
Index: tornado-6.4/tornado/httputil.py
===================================================================
--- tornado-6.4.orig/tornado/httputil.py
+++ tornado-6.4/tornado/httputil.py
@@ -791,6 +791,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.
 
@@ -800,6 +817,9 @@ class ParseBodyConfig:
     multipart: ParseMultipartConfig = dataclasses.field(
         default_factory=ParseMultipartConfig
     )
+    urlencoded: ParseUrlEncodedConfig = dataclasses.field(
+        default_factory=ParseUrlEncodedConfig
+    )
     """Configuration for ``multipart/form-data`` request bodies."""
 
 
@@ -858,7 +878,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():
Index: tornado-6.4/tornado/test/httputil_test.py
===================================================================
--- tornado-6.4.orig/tornado/test/httputil_test.py
+++ tornado-6.4/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"""\
