From 9c163aebeaad9e6e7d28bac1f33580eb00b0e421 Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Wed, 10 Dec 2025 15:15:25 -0500 Subject: [PATCH] web: Harden against invalid HTTP reason phrases We allow applications to set custom reason phrases for the HTTP status line (to support custom status codes), but if this were exposed to untrusted data it could be exploited in various ways. This commit guards against invalid reason phrases in both HTTP headers and in error pages. CVE: CVE-2025-67724 Upstream: https://github.com/tornadoweb/tornado/commit/9c163aebeaad9e6e7d28bac1f33580eb00b0e421 Signed-off-by: Thomas Perale --- tornado/web.py | 25 +++++++++++++++++++------ 1 files changed, 19 insertions(+), 6 deletions(-) diff --git a/tornado/web.py b/tornado/web.py index 2f702d6480..2351afdbe2 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -359,8 +359,10 @@ def set_status(self, status_code: int, reason: Optional[str] = None) -> None: :arg int status_code: Response status code. :arg str reason: Human-readable reason phrase describing the status - code. If ``None``, it will be filled in from - `http.client.responses` or "Unknown". + code (for example, the "Not Found" in ``HTTP/1.1 404 Not Found``). + Normally determined automatically from `http.client.responses`; this + argument should only be used if you need to use a non-standard + status code. .. versionchanged:: 5.0 @@ -369,6 +371,14 @@ def set_status(self, status_code: int, reason: Optional[str] = None) -> None: """ self._status_code = status_code if reason is not None: + if "<" in reason or not httputil._ABNF.reason_phrase.fullmatch(reason): + # Logically this would be better as an exception, but this method + # is called on error-handling paths that would need some refactoring + # to tolerate internal errors cleanly. + # + # The check for "<" is a defense-in-depth against XSS attacks (we also + # escape the reason when rendering error pages). + reason = "Unknown" self._reason = escape.native_str(reason) else: self._reason = httputil.responses.get(status_code, "Unknown") @@ -1345,7 +1355,8 @@ def send_error(self, status_code: int = 500, **kwargs: Any) -> None: reason = exception.reason self.set_status(status_code, reason=reason) try: - self.write_error(status_code, **kwargs) + if status_code != 304: + self.write_error(status_code, **kwargs) except Exception: app_log.error("Uncaught exception in write_error", exc_info=True) if not self._finished: @@ -1373,7 +1384,7 @@ def write_error(self, status_code: int, **kwargs: Any) -> None: self.finish( "%(code)d: %(message)s" "%(code)d: %(message)s" - % {"code": status_code, "message": self._reason} + % {"code": status_code, "message": escape.xhtml_escape(self._reason)} ) @property @@ -2520,9 +2531,11 @@ class HTTPError(Exception): mode). May contain ``%s``-style placeholders, which will be filled in with remaining positional parameters. :arg str reason: Keyword-only argument. The HTTP "reason" phrase - to pass in the status line along with ``status_code``. Normally + to pass in the status line along with ``status_code`` (for example, + the "Not Found" in ``HTTP/1.1 404 Not Found``). Normally determined automatically from ``status_code``, but can be used - to use a non-standard numeric code. + to use a non-standard numeric code. This is not a general-purpose + error message. """ def __init__(