# Tornado: Urlencoded body parsing omits max_num_fields, so one request can stall the event loop

Tornado's urlencoded body parser omits max_num_fields, so a 100MB separator-only POST creates ~50M fields and blocks the single-threaded event loop.

- Payout: Research
- Severity: high
- Program: Tornado
- Found by: arpitjain099
- Disclosed: 2026-09-02
- CVE: CVE-2026-82397
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-mpf4-983q-p7j4
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-mpf4-983q-p7j4
- Tags: area:web domain:web impact:dos kind:research platform:ghsa sev:high tech:python technique:source-review vuln:dos

**Target:** Python web framework HTTP request body parser (application/x-www-form-urlencoded)
**Root cause:** parse_qs_bytes calls urllib.parse.parse_qs without max_num_fields, so field count is bounded only by the 100MB default body cap; parsing is synchronous on the event loop before handler dispatch.
**Steps:**
1. Send a POST with Content-Type: application/x-www-form-urlencoded and a body of ~100MB consisting almost entirely of `&` separators (yields ~50M empty fields).
2. Body is read up to max_buffer_size (104857600) via http1connection.py:136-140.
3. RequestHandler._execute (web.py:1821) -> HTTPServerRequest._parse_body (httputil.py:636) -> parse_body_arguments -> parse_qs_bytes (httputil.py:1030) -> parse_qs in escape.py with no max_num_fields.
4. Parse runs synchronously on the event loop; all other connections stall until it completes.
**How found:** Code reading of the urlencoded parse path: noticed parse_qs was called without the max_num_fields parameter CPython added for this purpose, then traced the pre-dispatch call chain and the only bound (default 100MB body cap) to size the field count.
**Exploitation:** Unauthenticated attacker sends one large separator-heavy form POST to any route accepting form posts; the synchronous parse monopolizes the single-threaded IOLoop, denying service to all concurrent connections. Repeat requests sustain the stall.
**Insight:** When auditing parsers, check whether the stdlib's own DoS-guard parameters (max_num_fields, max_line_length, etc.) are actually forwarded; any synchronous parse reachable pre-dispatch on a single-threaded server is a whole-process DoS, not a per-request one.

