{"id":"CVE-2026-82397","aliases":["GHSA-mpf4-983q-p7j4","PYSEC-2026-3928"],"url":"https://o3.security/vulnerability/CVE-2026-82397","summary":"Tornado: Urlencoded body parsing omits max_num_fields, so one request can stall the event loop","details":"## Summary\n\nTornado parses `application/x-www-form-urlencoded` bodies with `urllib.parse.parse_qs` and does not pass `max_num_fields`. A body made almost entirely of separators produces tens of millions of fields, and the parse happens on the event loop before the handler runs, so a single request stalls the whole server.\n\n## Where it is\n\n`tornado/escape.py`, at HEAD `e530031405e2154654dedc4c84d5656b557ea310`:\n\n```python\nresult = urllib.parse.parse_qs(\n    qs, keep_blank_values, strict_parsing, encoding=\"latin1\", errors=\"strict\"\n)\n```\n\n`max_num_fields` is the parameter CPython added for exactly this, and it is absent.\n\nThe path to it is entirely server-side and pre-dispatch. `RequestHandler._execute` parses the body at `tornado/web.py:1821`, which reaches `HTTPServerRequest._parse_body` at `tornado/httputil.py:636`, and the urlencoded branch of `parse_body_arguments` calls `parse_qs_bytes` at `tornado/httputil.py:1030`.\n\nThe size that reaches it is bounded only by the body cap, which defaults to the stream's `max_buffer_size` of 104857600 at `tornado/iostream.py:239`, applied as the request body default at `tornado/http1connection.py:136-140`. A 100 MB body of separators is around fifty million fields.\n\n## Impact\n\nDenial of service against the whole process, not one request. Tornado is single-threaded and the parse is synchronous on the event loop, so every other connection waits. No authentication is needed if any route accepts a form post, which is the normal case.\n\n## Suggested fix\n\nPass a bound:\n\n```python\nresult = urllib.parse.parse_qs(\n    qs, keep_blank_values, strict_parsing, encoding=\"latin1\", errors=\"strict\",\n    max_num_fields=max_num_fields,\n)\n```\n\nwith a conservative default and a way for applications to raise it. CPython raises `ValueError` when the limit is exceeded, which maps cleanly onto a 400.\n\nLowering the default body cap for urlencoded specifically would help too, since 100 MB of form fields is not a shape any real client sends.\n\n## Why I do not think this is a duplicate\n\nThe published tornado advisories cover out-of-bounds access in the C extension, unbounded accumulation of decompressed chunks in `AsyncHTTPClient`, the Authorization header surviving cross-origin redirects, credential leakage on curl handle reuse, and cookie attribute validation. The decompression one is the nearest in spirit and is on the client side; this is the server parsing a request body. The call is unchanged at HEAD.","published":"2026-08-31T21:28:13.958Z","modified":"2026-10-13T17:27:09.875280702Z","cvss":{"score":7.5,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"tornado","fixedVersion":"6.5.8"}],"fix":{"url":"https://github.com/tornadoweb/tornado/commit/8d6363ed7b69d5f0da806efe34d256627a2191de","label":"tornadoweb/tornado@8d6363e"},"references":[{"type":"WEB","url":"https://github.com/tornadoweb/tornado/releases/tag/v6.5.8"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/82xxx/CVE-2026-82397.json"},{"type":"ADVISORY","url":"https://github.com/tornadoweb/tornado/security/advisories/GHSA-mpf4-983q-p7j4"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-82397"},{"type":"FIX","url":"https://github.com/tornadoweb/tornado/commit/8d6363ed7b69d5f0da806efe34d256627a2191de"},{"type":"FIX","url":"https://github.com/tornadoweb/tornado/pull/3704"},{"type":"PACKAGE","url":"https://github.com/tornadoweb/tornado"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-13T17:27:09.875280702Z"}}