CVE-2026-126347: CVE-2026-126347: Open Redirect via Triple-Slash Redirect Paths in Tornado RedirectHandler
CVE-2026-126347: Open Redirect via Triple-Slash Redirect Paths in Tornado RedirectHandler Vulnerability ID: CVE-2026-126347 CVSS Score: 3.1 Published: 2026-11-13 CVE-2026-126347 (GHSA-5cjw-f85j-cv6r) is an open redi
CVE-2026-126347: Open Redirect via Triple-Slash Redirect Paths in Tornado RedirectHandler
Vulnerability ID: CVE-2026-126347
CVSS Score: 3.1
Published: 2026-11-13
CVE-2026-126347 (GHSA-5cjw-f85j-cv6r) is an open redirect vulnerability (CWE-601) in Tornado, an asynchronous Python networking library and web framework. The flaw resides in tornado.web.RedirectHandler when dynamic URL redirection templates incorporate unvalidated user input. Due to a parser differential between RFC 3986 (used by Python's urllib.parse.urlsplit) and the WHATWG URL Standard (used by modern web browsers), inputs containing three or more leading slashes bypass Tornado 6.6.0's validation logic, allowing unauthenticated attackers to redirect users to arbitrary external domains.
TL;DR
Tornado versions prior to 6.6.1 are vulnerable to an open redirect in tornado.web.RedirectHandler when route templates accept dynamic user substitutions. Supplying three or more leading slashes (e.g., ///evil.com) exploits a parsing discrepancy between Python's urllib.parse.urlsplit and modern web browsers, bypassing validation introduced in version 6.6.0 for CVE-2026-116578.
β οΈ Exploit Status: POC
Technical Details
- Vulnerability Class: CWE-601: URL Redirection to Untrusted Site ('Open Redirect')
- Affected Component: tornado.web.RedirectHandler (<= 6.6.0)
- Attack Vector: Network (Unauthenticated HTTP GET with multi-slash path payload)
- CVSS v3.1 Score: 3.1 Low (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N)
- EPSS Score: 0.16% (5.59th Percentile)
- Root Cause: RFC 3986 (urllib.parse.urlsplit) vs. WHATWG URL parser differential on 3+ leading slashes
- Exploit Status: PoC Available (Unit tests in commit c503241622c0efe7c40110d89834ce8b7fead1e4)
- Patched Version: Tornado 6.6.1
Affected Systems
- Tornado Web Framework (Python package: tornado)
-
tornado: <= 6.6.0 (Fixed in:
6.6.1)
Code Analysis
Commit: c503241
Collapse multiple leading slashes in _scheme_and_netloc and validate RedirectHandler templates using static literal prefix before first substitution.
diff --git a/tornado/web.py b/tornado/web.py
index e6387037..613a0c20 100644
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -72,6 +72,7 @@
import re
import socket
import stat
+import string
import sys
import threading
import time
@@ -2848,23 +2849,40 @@ def initialize(self, url: str, permanent: bool = True) -> None:
def get(self, *args: Any, **kwargs: Any) -> None:
to_url = self._url.format(*args, **kwargs)
# The scheme and netloc of the redirect must come from the template, not
- # from the request. Find out what they are by formatting the template
- # with a placeholder that cannot extend a scheme or netloc.
- expected_url = self._url.format(
- *("/" for _ in args), **{k: "/" for k in kwargs}
- )
+ # from the request. Find out what they are from the part of the template
+ # before the first substitution. (Formatting the whole template with a
+ # placeholder doesn't work: whatever the placeholder is, it can turn the
+ # template into a URL with a different netloc, as "/" does for "/{0}/b".)
+ prefix = ""
+ has_substitution = False
+ for literal, field, _, _ in string.Formatter().parse(self._url):
+ prefix += literal
+ if field is not None:
+ has_substitution = True
+ break
try:
- unchanged = _scheme_and_netloc(to_url) == _scheme_and_netloc(expected_url)
+ if self.request.query_arguments:
+ # Query arguments are added before the check because url_concat
+ # can change the rest of the URL.
+ # TODO: figure out typing for the next line.
+ to_url = httputil.url_concat(
+ to_url,
+ list(httputil.qs_to_qsl(self.request.query_arguments)), # type: ignore
+ )
+ scheme, netloc = _scheme_and_netloc(prefix)
+ unchanged = _scheme_and_netloc(to_url) == (scheme, netloc)
+ if has_substitution and scheme in ("http", "https") and not netloc:
+ # The host would come entirely from a substitution (or a
+ # browser would find one where urlsplit doesn't, as in
+ # "https:///host").
+ unchanged = False
except ValueError:
unchanged = False
if not unchanged:
raise HTTPError(400, "Redirect URL cannot change the scheme or netloc")
- if self.request.query_arguments:
- # TODO: figure out typing for the next line.
- to_url = httputil.url_concat(
- to_url,
- list(httputil.qs_to_qsl(self.request.query_arguments)), # type: ignore
- )
+ if httputil._FORBIDDEN_HEADER_CHARS_RE.search(to_url):
+ # Let this be a 400 rather than a 500 from set_header.
+ raise HTTPError(400, "Redirect URL contains invalid characters")
self.redirect(to_url, permanent=self._permanent)
@@ -4251,5 +4269,10 @@ def _scheme_and_netloc(url: str) -> tuple[str, str]:
Both are empty for a relative URL. Raises `ValueError` for some malformed
URLs.
"""
- parts = urllib.parse.urlsplit(_normalize_url_like_browser(url))
+ url = _normalize_url_like_browser(url)
+ # urlsplit follows RFC 3986, in which "///example.com" has an empty netloc,
+ # but browsers resolving it against an http or https URL skip any number of
+ # slashes before the host. Collapse them so urlsplit sees the same host.
+ url = re.sub(r"^//+", "//", url)
+ parts = urllib.parse.urlsplit(url)
return parts.scheme.lower(), parts.netloc.lower()
Exploit Details
- GitHub: Official Tornado fix commit containing unit tests and functional reproduction payloads in tornado/test/web_test.py
Mitigation Strategies
- Upgrade Tornado to version 6.6.1 or newer to ensure multi-slash prefixes are collapsed prior to RFC 3986 URL splitting.
- Avoid configuring tornado.web.RedirectHandler templates where dynamic substitutions appear at the start of the URL or immediately after a leading slash.
- Restrict route regular expressions feeding into RedirectHandler to alphanumeric characters (
[a-zA-Z0-9_-]+) rather than wildcard capture groups (.*). - Deploy Web Application Firewall (WAF) rules to inspect and block requests containing multiple consecutive raw or URL-encoded slashes and backslashes targeting redirect endpoints.
Remediation Steps:
- Audit application dependency manifests (requirements.txt, pyproject.toml, Pipfile.lock) to identify tornado versions <= 6.6.0.
- Run
pip install --upgrade 'tornado>=6.6.1'across all affected Python virtual environments and container images. - Inspect all
tornado.web.RedirectHandlerroute declarations in the codebase to ensure templates do not use unvalidated host substitutions such ashttps://{0}orhttps:{0}. - Verify remediation by sending a test request with an encoded double-slash payload (e.g.,
curl -I 'https://target.example/redirect/%2F%2Fattacker.com') and confirming the server returnsHTTP/1.1 400 Bad Request.
References
- CVE Record (CVE.org): CVE-2026-126347
- GitHub Security Advisory: GHSA-5cjw-f85j-cv6r
- Tornado Fix Commit c503241622c0efe7c40110d89834ce8b7fead1e4
- Tornado v6.6.1 Release Tag
- CVEListV5 Record: CVE-2026-126347.json
- NVD Vulnerability Detail: CVE-2026-126347
Read the full report for CVE-2026-126347 on our website for more details including interactive diagrams and full exploit analysis.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.