CVE-2025-67724
Tornado: Unescaped reason phrase leads to header injection and XSS.
- CVSS 6.1
- CWE-79
- Input Validation and Sanitization
- Remote
Tornado is a Python web framework and asynchronous networking library. In versions 6.5.2 and below, the supplied reason phrase is used unescaped in HTTP headers (where it could be used for header injection) or in HTML in the default error page (where it could be used for XSS) and can be exploited by passing untrusted or malicious data into the reason argument. Used by both RequestHandler.set_status and tornado.web.HTTPError, the argument is designed to allow applications to pass custom "reason" phrases (the "Not Found" in HTTP/1.1 404 Not Found) to the HTTP status line (mainly for non-standard status codes). This issue is fixed in version 6.5.3.
- CWE
- CWE-79
- CVSS base score
- 6.1
- Published
- 2025-12-12
- OWASP
- A03 Injection
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Cross-Site Scripting (XSS)
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- Tornado
- Fixed by upgrading
- Yes
Solution
Upgrade Tornado to version 6.5.3 or later.
Vulnerable code sample
import tornado.ioloop
import tornado.web
import tornado.httpserver
# This code represents the vulnerable state of Tornado (<= 6.5.2)
# as described in CVE-2025-67724.
# The 'reason' argument is not escaped, leading to Header Injection or XSS.
class VulnerableHandler(tornado.web.RequestHandler):
def get(self):
# The payload is taken directly from user input.
payload = self.get_argument("payload", "default")
vuln_type = self.get_argument("type", "xss")
if vuln_type == "header_injection":
# Demonstrates Header Injection.
# A payload like "Payload\r\nX-Injected-Header: InjectedValue"
# will add a new header to the response.
self.set_status(404, reason=payload)
self.write("Check the response headers for an injected header.")
else:
# Demonstrates XSS (default).
# A payload like "<script>alert('XSS')</script>" will be
# rendered unescaped on the default error page.
raise tornado.web.HTTPError(status_code=404, reason=payload)
def make_app():
return tornado.web.Application([
(r"/", VulnerableHandler),
])
if __name__ == "__main__":
app = make_app()
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(8888)
# To test XSS:
# http://127.0.0.1:8888/?type=xss&payload=%3Cscript%3Ealert(%27XSS%27)%3C/script%3E
# To test Header Injection (use curl):
# curl -v "http://127.0.0.1:8888/?type=header_injection&payload=Vulnerable%0d%0AX-Injected-Header:%20InjectedValue"
tornado.ioloop.IOLoop.current().start()Patched code sample
import html
import re
import tornado.web
import tornado.ioloop
import tornado.httpserver
class VulnerableHandler(tornado.web.RequestHandler):
def get(self):
"""
This handler demonstrates the vulnerability.
A malicious 'reason' phrase is passed directly to set_status.
In vulnerable versions, this is not escaped.
"""
# Payload for XSS injection into the error page
xss_payload = "Not Found<script>alert('XSS vulnerability demonstrated')</script>"
# Payload for HTTP Header Injection (CRLF injection)
header_injection_payload = "OK\r\nInjected-Header: Malicious-Value"
payload_type = self.get_argument("payload", "xss")
if payload_type == "xss":
# This would trigger an XSS vulnerability on the default error page
# in unpatched versions of Tornado.
self.set_status(404, reason=xss_payload)
elif payload_type == "header":
# This would trigger an HTTP header injection in unpatched versions.
# We use a 200 status code to show the reason phrase in the status line.
self.set_status(200, reason=header_injection_payload)
self.write("Check the raw HTTP response for an injected header.")
else:
self.set_status(400, "Invalid payload type specified.")
class FixedHandler(tornado.web.RequestHandler):
def _sanitize_reason(self, reason):
"""
Represents the fix applied in patched Tornado versions.
It removes control characters like carriage returns and newlines
to prevent HTTP header injection (CRLF injection).
The default error page template in Tornado also performs HTML escaping,
which prevents XSS. This function focuses on the header part of the fix.
"""
return re.sub(r"[\r\n]", " ", reason)
def set_status(self, status_code, reason=None):
"""
Overrides the default set_status to demonstrate the fix.
The 'reason' is sanitized before being passed to the original method.
"""
if reason:
sanitized_reason = self._sanitize_reason(reason)
# In the actual Tornado fix, this sanitization happens internally.
# We are calling the parent method with the sanitized reason.
super().set_status(status_code, reason=sanitized_reason)
else:
super().set_status(status_code, reason=reason)
def write_error(self, status_code, **kwargs):
"""
Overrides write_error to demonstrate the XSS fix.
The reason phrase is fetched and explicitly HTML-escaped before rendering.
Patched Tornado versions ensure this escaping happens in the default template.
"""
reason = kwargs.get('reason', 'An error occurred')
# Manually apply the HTML escaping fix
escaped_reason = html.escape(reason)
self.set_header('Content-Type', 'text/html')
self.finish(f"""
<html>
<title>{status_code}: {escaped_reason}</title>
<body>
<h2>HTTP Error {status_code}: {escaped_reason}</h2>
<p>The 'reason' has been HTML-escaped, preventing XSS.</p>
</body>
</html>
""")
def get(self):
"""
This handler demonstrates the fix.
The same malicious payloads are used, but the overridden methods
apply sanitization and escaping.
"""
# Payload for XSS injection
xss_payload = "Not Found<script>alert('This should not execute')</script>"
# Payload for HTTP Header Injection
header_injection_payload = "OK\r\nInjected-Header: Malicious-Value"
payload_type = self.get_argument("payload", "xss")
try:
if payload_type == "xss":
# The custom write_error will be triggered by raising an HTTPError.
raise tornado.web.HTTPError(404, reason=xss_payload)
elif payload_type == "header":
# The overridden set_status will sanitize the reason.
self.set_status(200, reason=header_injection_payload)
self.write("Check the raw HTTP response. The header injection is prevented.")
else:
self.set_status(400, "Invalid payload type specified.")
except tornado.web.HTTPError as e:
self.write_error(e.status_code, reason=e.reason)
def make_app():
return tornado.web.Application([
(r"/vulnerable", VulnerableHandler),
(r"/fixed", FixedHandler),
])
if __name__ == "__main__":
app = make_app()
http_server = tornado.httpserver.HTTPServer(app)
port = 8888
http_server.listen(port)
print(f"Server running on http://localhost:{port}")
print("Demonstrate vulnerability: http://localhost:8888/vulnerable?payload=xss")
print("Demonstrate fix: http://localhost:8888/fixed?payload=xss")
print("\nTo test header injection, use curl:")
print("Vulnerable: curl -v 'http://localhost:8888/vulnerable?payload=header'")
print("Fixed: curl -v 'http://localhost:8888/fixed?payload=header'")
tornado.ioloop.IOLoop.current().start()Payload
<script>alert('XSS')</script>
Cite this entry
@misc{vaitp:cve202567724,
title = {{Tornado: Unescaped reason phrase leads to header injection and XSS.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-67724},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-67724/}}
}
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 ::
