CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-126347

CVE-2026-126347: Open Redirect via Triple-Slash Redirect Paths in Tornado RedirectHandler

Alon Barad
Alon Barad
Software Engineer

Nov 14, 2026·11 min read·1 visit

Executive Summary (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.

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.

Vulnerability Overview

Tornado is an asynchronous networking library and web application framework for Python designed to handle high-concurrency network traffic. Within its core web routing package, the tornado.web.RedirectHandler class provides a declarative mechanism to redirect incoming HTTP GET requests to a specified target URL. Developers can configure RedirectHandler with static destination strings or dynamic format templates that interpolate regex capture groups from the request URI path using standard Python str.format() syntax.

CVE-2026-126347 (tracked also as GHSA-5cjw-f85j-cv6r) is an open redirect vulnerability classified under CWE-601 (URL Redirection to Untrusted Site). When an application defines a RedirectHandler route where user-controlled path parameters are substituted at the beginning of a path or immediately following a leading slash (such as /{0}/destination or {0}), an attacker can supply crafted path segments containing multiple leading slashes or backslashes. Because the server fails to properly identify the target authority within multi-slash sequences, it emits an HTTP 301 Moved Permanently or 302 Found response with a Location header pointing to an attacker-controlled domain.

This vulnerability is the direct result of an incomplete remediation for CVE-2026-116578, which was patched in Tornado 6.6.0. While the 6.6.0 patch attempted to ensure that dynamic template substitutions could not modify the expected URL scheme or network location (netloc), the validation logic relied directly on Python's urllib.parse.urlsplit. Attackers can bypass this check by supplying three or more leading slashes (e.g., ///attacker.com), exploiting a specification divergence between server-side URL parsing and client-side browser navigation.

Root Cause Analysis: RFC 3986 vs. WHATWG Parser Differential

The technical root cause of CVE-2026-126347 lies in a fundamental parsing discrepancy between RFC 3986 (Uniform Resource Identifier: Generic Syntax), which governs Python's standard urllib.parse.urlsplit function, and the WHATWG URL Living Standard, which dictates how modern web browsers parse and resolve URLs.

Under RFC 3986 Section 3, a network-path reference (protocol-relative URL) begins with exactly two forward slashes (//) followed by the authority component (netloc), which is terminated by the next slash, question mark, or hash character. When urllib.parse.urlsplit processes a string beginning with three or more slashes, such as ///example.com/b, it sees an empty string between the second and third slashes. Consequently, urlsplit concludes that no authority is present, sets netloc to an empty string (""), and assigns the third slash and all subsequent characters (///example.com/b or /example.com/b depending on normalization) to the URL path component.

>>> import urllib.parse
>>> urllib.parse.urlsplit("///example.com/b")
SplitResult(scheme='', netloc='', path='///example.com/b', query='', fragment='')

Conversely, web browsers conforming to the WHATWG URL Standard implement state machine rules for relative URL resolution that treat any number of consecutive leading forward slashes (/) and backslashes (\) after a scheme or at the start of a relative reference as equivalent to a two-slash authority delimiter. When a browser receives an HTTP redirect containing Location: ///example.com/b or Location: ////example.com/b and resolves it against an http: or https: base URL, it strips all leading slashes and parses example.com as the target host, navigating the user to https://example.com/b.

In Tornado 6.6.0, the validation function _scheme_and_netloc(url) checked whether the formatted destination URL shared the same (scheme, netloc) tuple as an expected template placeholder formatted with "/". Because ///example.com/b evaluated to ("", "") in urlsplit, it matched the ("", "") tuple of a relative path template, allowing the protocol-relative redirect payload to pass validation unchallenged.

Furthermore, the 6.6.0 remediation contained a functional design flaw in how it constructed expected_url. By substituting "/" for every positional and keyword argument across the entire format string, a legitimate template such as /{0}/b was transformed into ///b. Similarly, a template such as /{0}.html became //.html, which urlsplit parsed as having netloc=".html". This caused valid, non-malicious requests to /{0}.html (where the actual formatted URL had netloc="") to fail the equality check and erroneously return HTTP 400 Bad Request errors.

Code Analysis: Vulnerable Logic vs. Patch Implementation

In Tornado 6.6.0, RedirectHandler.get() validated redirects by comparing the scheme and network location of the user-formatted URL (to_url) against a dummy-formatted version (expected_url), and appended query parameters only after the check was performed:

# Vulnerable implementation in tornado/web.py (Tornado 6.6.0)
def get(self, *args: Any, **kwargs: Any) -> None:
    to_url = self._url.format(*args, **kwargs)
    # Flaw 1: Formatting with "/" alters netloc of templates like "/{0}.html" -> "//.html"
    expected_url = self._url.format(
        *("/" for _ in args), **{k: "/" for k in kwargs}
    )
    try:
        # Flaw 2: _scheme_and_netloc("///evil.com") returns ("", ""), bypassing validation
        unchanged = _scheme_and_netloc(to_url) == _scheme_and_netloc(expected_url)
    except ValueError:
        unchanged = False
    if not unchanged:
        raise HTTPError(400, "Redirect URL cannot change the scheme or netloc")
    # Flaw 3: Query arguments concatenated after validation can mutate URL structure
    if self.request.query_arguments:
        to_url = httputil.url_concat(
            to_url,
            list(httputil.qs_to_qsl(self.request.query_arguments)),
        )
    self.redirect(to_url, permanent=self._permanent)
 
def _scheme_and_netloc(url: str) -> tuple[str, str]:
    parts = urllib.parse.urlsplit(_normalize_url_like_browser(url))
    return parts.scheme.lower(), parts.netloc.lower()

Commit c503241622c0efe7c40110d89834ce8b7fead1e4 (released in Tornado 6.6.1) resolves the vulnerability through four structural changes in tornado/web.py. First, _scheme_and_netloc() is updated to collapse any sequence of two or more leading slashes (^//+) down to exactly two slashes (//) after running _normalize_url_like_browser() (which already normalizes backslashes and strips control characters). This forces urllib.parse.urlsplit to parse multi-slash prefixes (///evil.com) identically to modern browsers, extracting evil.com as the netloc.

# Patched _scheme_and_netloc in tornado/web.py (Tornado 6.6.1)
def _scheme_and_netloc(url: str) -> tuple[str, str]:
    url = _normalize_url_like_browser(url)
    # Collapse 2+ leading slashes so urlsplit sees the same host as browsers
    url = re.sub(r"^//+", "//", url)
    parts = urllib.parse.urlsplit(url)
    return parts.scheme.lower(), parts.netloc.lower()

Second, instead of formatting the template with dummy "/" strings, RedirectHandler.get() uses string.Formatter().parse(self._url) to extract only the static literal prefix preceding the very first format field. Because security policy dictates that the scheme and host must be defined statically by the template prior to any dynamic parameter substitution, evaluating _scheme_and_netloc(prefix) provides an immutable baseline without inducing false positives on templates like /{0}.html.

# Patched RedirectHandler.get in tornado/web.py (Tornado 6.6.1)
def get(self, *args: Any, **kwargs: Any) -> None:
    to_url = self._url.format(*args, **kwargs)
    prefix = ""
    has_substitution = False
    # Extract only the static literal prefix before the first format field
    for literal, field, _, _ in string.Formatter().parse(self._url):
        prefix += literal
        if field is not None:
            has_substitution = True
            break
    try:
        if self.request.query_arguments:
            # Apply url_concat before validation to catch urlunsplit mutations
            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)
        # Explicitly block hostless http/https prefixes where substitution supplies the host
        if has_substitution and scheme in ("http", "https") and not netloc:
            unchanged = False
    except ValueError:
        unchanged = False
    if not unchanged:
        raise HTTPError(400, "Redirect URL cannot change the scheme or netloc")
    # Reject forbidden header characters with 400 instead of 500 in set_header
    if httputil._FORBIDDEN_HEADER_CHARS_RE.search(to_url):
        raise HTTPError(400, "Redirect URL contains invalid characters")
    self.redirect(to_url, permanent=self._permanent)

Third, the patch adds an explicit check (if has_substitution and scheme in ("http", "https") and not netloc: unchanged = False) to reject templates such as https://{0} or https:{0}. In such templates, prefix has scheme="https" and netloc="". If a user supplied ////evil.com to https:{0}, or if httputil.url_concat re-serialized https:evil.com?x=1 via urlunsplit, a mismatch or host injection would occur. Moving httputil.url_concat prior to _scheme_and_netloc(to_url) and rejecting empty netloc values on http/https templates with substitutions closes these variant vectors completely. Finally, checking httputil._FORBIDDEN_HEADER_CHARS_RE ensures that injected control characters (%0A, %00) return a clean 400 Bad Request rather than an unhandled 500 Internal Server Error inside set_header().

Exploitation Methodology and Proof-of-Concept

Exploiting CVE-2026-126347 requires no authentication, but depends on the target application exposing a tornado.web.RedirectHandler route where user-controlled path capture groups can control the beginning of the redirect path or immediately follow the initial slash. Because Tornado URL-decodes regex capture groups before passing them to RedirectHandler.get(*args), an attacker can use raw slashes or URL-encoded characters (%2F for /, %5C for \, %09 for horizontal tab) to traverse or match route regular expressions that might otherwise split on unencoded slashes.

Consider a vulnerable Tornado application configured with the following routing table:

import tornado.ioloop
import tornado.web
 
app = tornado.web.Application([
    (r"/redirect/(.*)", tornado.web.RedirectHandler, {"url": "/{0}/destination"}),
    (r"/any/(.*)", tornado.web.RedirectHandler, {"url": "{0}"}),
    (r"/schemeonly/(.*)", tornado.web.RedirectHandler, {"url": "https:{0}"}),
])

To exploit the /redirect/(.*) endpoint, which prepends a single static slash to the captured argument (/{0}/destination), an attacker sends an HTTP GET request containing two or more leading slashes in the capture group, URL-encoded as %2F%2F so the request path is normalized cleanly by upstream proxies:

GET /redirect/%2F%2Fevil.com HTTP/1.1
Host: vulnerable-tornado-app.example
User-Agent: Mozilla/5.0

When Tornado processes this request on versions <= 6.6.0:

  1. The router matches /redirect/(.*) and URL-decodes %2F%2Fevil.com into //evil.com.
  2. RedirectHandler.get() formats /{0}/destination with //evil.com, producing to_url = "///evil.com/destination".
  3. _scheme_and_netloc("///evil.com/destination") calls urllib.parse.urlsplit("///evil.com/destination"), which returns scheme="" and netloc="".
  4. Because expected_url (///destination) also yields ("", ""), the validation check passes.
  5. Tornado responds with HTTP/1.1 301 Moved Permanently and Location: ///evil.com/destination, causing the victim's browser to navigate to https://evil.com/destination.

Additional bypass payloads confirmed in the official Tornado regression suite (tornado/test/web_test.py) demonstrate alternative smuggling techniques using backslashes, horizontal tabs, and scheme-only templates:

  • Direct Triple/Quadruple Slash on Root Template (/any/(.*)): GET /any////evil.com/ HTTP/1.1 -> Location: ////evil.com/
  • Encoded Slash and Backslash Combinations: GET /redirect/%2F%5Cevil.com HTTP/1.1 or GET /a/%2F%2F%5Cexample.com/b HTTP/1.1
  • Tab Character Smuggling (Ignored by WHATWG URL Parser): GET /redirect/%09//evil.com HTTP/1.1
  • Scheme-Only Template Exploitation (/schemeonly/(.*)): GET /schemeonly////evil.com HTTP/1.1 or GET /schemeonly/evil.com?x=1 HTTP/1.1

Impact Assessment

CVE-2026-126347 is rated Low severity with a CVSS v3.1 base score of 3.1 (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N). The Attack Complexity is scored as High (AC:H) because exploitation is strictly conditional on an application explicitly configuring tornado.web.RedirectHandler with dynamic format strings where user input can influence the authority boundary. Applications that only use RedirectHandler with static target URLs or place substitutions exclusively within query parameters or deep path segments (e.g., /static/prefix/{0}) are not affected.

When a vulnerable route configuration is present, the primary security impact is the facilitation of phishing and credential harvesting attacks (MITRE ATT&CK T1566.002: Spearphishing Link). Because the initial URL inside a phishing link points to a legitimate, trusted domain owned by the target organization, automated email security gateways and end users are more likely to trust the link. Upon clicking the link, the user's browser is transparently redirected to an attacker-controlled external site that can mimic the organization's login portal.

In more complex architectures, open redirects within trusted origin endpoints can also be chained with secondary vulnerabilities. For example, if an OAuth 2.0 or OpenID Connect identity provider validates redirect_uri parameters using permissive prefix matching on the application domain, an attacker can point the redirect_uri to the vulnerable RedirectHandler endpoint and use query parameter forwarding (which RedirectHandler performs automatically when self.request.query_arguments is populated) to leak authorization codes or tokens via the Location header or Referer leakage.

According to FIRST Exploit Prediction Scoring System (EPSS) metrics, CVE-2026-126347 holds an EPSS score of 0.00163 (0.16%), placing it in the 5.59th percentile. The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog, and there is no evidence of active exploitation in ransomware campaigns.

Remediation and Mitigation Guidance

The definitive remediation for CVE-2026-126347 is to upgrade the tornado package to version 6.6.1 or later. Version 6.6.1 incorporates commit c503241622c0efe7c40110d89834ce8b7fead1e4, which normalizes multi-slash prefixes prior to urlsplit evaluation and validates destination URLs against the static literal prefix of the RedirectHandler template.

pip install --upgrade "tornado>=6.6.1"

> [!NOTE] > Upgrading from Tornado 6.6.0 to 6.6.1 also resolves a functional regression introduced in 6.6.0 where valid relative redirect templates such as /{0}.html erroneously returned HTTP 400 Bad Request.

For environments where an immediate upgrade to Tornado 6.6.1 is not possible, engineering teams can implement the following architectural workarounds and compensating controls:

  1. Avoid Root-Level Substitutions in RedirectHandler: Refactor route definitions so that dynamic substitutions occur only after a static path segment (e.g., use {"url": "/items/{0}/destination"} rather than {"url": "/{0}/destination"} or {"url": "{0}"}). When a static path segment such as /items/ precedes {0}, injecting leading slashes only results in /items////evil.com, which browsers treat as a harmless path on the same origin.
  2. Restrict Route Regular Expressions: Tighten the regular expression capture group in the Application routing table so that it cannot match forward slashes, backslashes, or whitespace characters. For example, replace (r"/(.*)/target", RedirectHandler, {"url": "/{0}/destination"}) with (r"/([a-zA-Z0-9_-]+)/target", RedirectHandler, {"url": "/{0}/destination"}).
  3. Subclass RedirectHandler to Strip Leading Slashes: If dynamic root-relative redirects are required on an older Tornado version, implement a custom subclass that strips all leading slashes, backslashes, and control characters from captured arguments before invoking super().get():
import re
import tornado.web
 
class SafeRedirectHandler(tornado.web.RedirectHandler):
    def get(self, *args, **kwargs):
        clean_args = [re.sub(r"^[/\\\s\x00-\x1f]+", "", a) if isinstance(a, str) else a for a in args]
        clean_kwargs = {k: (re.sub(r"^[/\\\s\x00-\x1f]+", "", v) if isinstance(v, str) else v) for k, v in kwargs.items()}
        super().get(*clean_args, **clean_kwargs)

Official Patches

tornadowebGitHub Security Advisory GHSA-5cjw-f85j-cv6r
tornadowebGit Fix Commit for CVE-2026-126347
tornadowebTornado v6.6.1 Release Tag

Fix Analysis (1)

Technical Appendix

CVSS Score
3.1/ 10
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N
EPSS Probability
0.16%
Top 94% most exploited

Affected Systems

Tornado Web Framework (Python package: tornado)

Affected Versions Detail

Product
Affected Versions
Fixed Version
tornado
tornadoweb
<= 6.6.06.6.1
AttributeDetail
Vulnerability ClassCWE-601: URL Redirection to Untrusted Site ('Open Redirect')
Affected Componenttornado.web.RedirectHandler (<= 6.6.0)
Attack VectorNetwork (Unauthenticated HTTP GET with multi-slash path payload)
CVSS v3.1 Score3.1 Low (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N)
EPSS Score0.16% (5.59th Percentile)
Root CauseRFC 3986 (urllib.parse.urlsplit) vs. WHATWG URL parser differential on 3+ leading slashes
Exploit StatusPoC Available (Unit tests in commit c503241622c0efe7c40110d89834ce8b7fead1e4)
Patched VersionTornado 6.6.1

MITRE ATT&CK Mapping

T1566.002Phishing: Spearphishing Link
Initial Access
CWE-601
URL Redirection to Untrusted Site ('Open Redirect')

A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.

Known Exploits & Detection

GitHubOfficial Tornado fix commit containing unit tests and functional reproduction payloads in tornado/test/web_test.py

Vulnerability Timeline

Fix commit c503241622c0efe7c40110d89834ce8b7fead1e4 authored and committed to tornadoweb/tornado
2026-10-29
Tornado 6.6.1 released, GHSA-5cjw-f85j-cv6r published, and CVE-2026-126347 assigned
2026-11-12
CVE-2026-126347 metadata updated in NVD and international vulnerability registries
2026-11-13

References & Sources

  • [1]CVE Record (CVE.org): CVE-2026-126347
  • [2]GitHub Security Advisory: GHSA-5cjw-f85j-cv6r
  • [3]Tornado Fix Commit c503241622c0efe7c40110d89834ce8b7fead1e4
  • [4]Tornado v6.6.1 Release Tag
  • [5]CVEListV5 Record: CVE-2026-126347.json
  • [6]NVD Vulnerability Detail: CVE-2026-126347
Related Vulnerabilities
CVE-2026-116578

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•about 2 hours ago•CVE-2026-128123
5.9

CVE-2026-128123: Uncaught Exception and Denial of Service in SvelteKit @sveltejs/adapter-node

A Denial of Service (DoS) vulnerability exists in @sveltejs/adapter-node versions 5.5.7 and 6.0.0-next.0 through 6.0.0. When a SvelteKit server route returns a direct response from a fetch() call with a Content-Type of text/event-stream, the Node.js adapter attempts to mutate the response headers to disable proxy buffering. Because headers returned directly by the WHATWG Fetch API carry an immutable guard, this mutation throws an unhandled TypeError, terminating the underlying Node.js process and denying service to all active and subsequent clients.

Alon Barad
Alon Barad
2 views•9 min read
•about 3 hours ago•CVE-2026-128122
5.9

CVE-2026-128122: Prerendered Redirect Rule Injection in @sveltejs/adapter-cloudflare

CVE-2026-128122 (GHSA-5wg4-q3ww-283h) is a build-time CRLF and wildcard injection vulnerability in `@sveltejs/adapter-cloudflare`. During the prerendering phase of a SvelteKit build, the adapter writes crawled redirect paths directly into the Cloudflare `_redirects` and `_routes.json` configuration files without output encoding or character validation. An attacker capable of controlling link paths on prerendered pages can inject newline characters, spaces, or wildcard patterns to forge arbitrary edge redirection rules, enabling asset hijacking and Stored Cross-Site Scripting (XSS).

Amit Schendel
Amit Schendel
2 views•10 min read
•about 4 hours ago•CVE-2026-128121
5.9

CVE-2026-128121: Regular Expression Injection in SvelteKit Vercel Route Configuration via @sveltejs/adapter-vercel and @sveltejs/adapter-static

A vulnerability classified under CWE-116 (Improper Encoding or Escaping of Output) and CWE-625 (Permissive Regular Expression) exists in the @sveltejs/adapter-vercel and @sveltejs/adapter-static packages for SvelteKit. During the build-time prerendering phase, pathnames discovered by the SvelteKit link crawler are inserted into the generated Vercel routing configuration (.vercel/output/config.json) without escaping regular expression metacharacters. Because the Vercel edge routing engine evaluates the 'src' property of route entries as regular expressions, an attacker capable of injecting links into prerendered application content can introduce arbitrary regular expression syntax, hijacking application routing and exposing unintended assets or responses.

Amit Schendel
Amit Schendel
3 views•11 min read
•about 5 hours ago•CVE-2026-128117
5.9

CVE-2026-128117: Unhandled URIError Denial of Service in SvelteKit Request Routing

CVE-2026-128117 (GHSA-mw6w-2fmv-pvw3) is a denial of service (DoS) vulnerability in SvelteKit (@sveltejs/kit) caused by an uncaught URIError exception during request routing. When an application implements a reroute hook that rewrites a request pathname containing invalid percent-encoding into a valid route, SvelteKit subsequently calls decode_pathname on the original unvalidated url.pathname outside of a try...catch block. In deployments utilizing @sveltejs/adapter-node, this unhandled exception terminates the Node.js runtime process.

Alon Barad
Alon Barad
3 views•9 min read
•about 6 hours ago•CVE-2026-126303
8.7

CVE-2026-126303: Uncontrolled Resource Consumption in NLTK SteppingRecursiveDescentParser

CVE-2026-126303 (GHSA-8fx7-8jr8-84rv) is an uncontrolled resource consumption vulnerability in the Natural Language Toolkit (NLTK) prior to version 3.10.4. The flaw is located in the SteppingRecursiveDescentParser.parse() method within the nltk.parse.recursivedescent module. Because the parser executes a top-down recursive descent stepping loop without enforcing an execution time deadline or step limit, processing tokens against a left-recursive, self-embedding, or ambiguous Context-Free Grammar (CFG) causes the parser to enter an infinite loop. This results in complete CPU core exhaustion and worker thread starvation, leading to an application-level denial of service.

Alon Barad
Alon Barad
4 views•10 min read
•about 7 hours ago•CVE-2026-62683
3.1

CVE-2026-62683: Incorrect Authorization via Stale Public Shares in File Browser

File Browser versions prior to 2.63.17 contain an authorization and state-management flaw in the BoltDB storage backend. When an authenticated user deletes a shared directory via an HTTP request with a trailing slash (e.g., DELETE /api/resources/a/), the share cleanup routine queries the database index using the unnormalized path before stripping the trailing slash. Because canonical share paths are stored without trailing slashes (e.g., /a), the database prefix query fails to match the parent directory share record. If a directory with the same name is later recreated, the dormant public share hash automatically reactivates and exposes the new directory contents to unauthenticated users.

Alon Barad
Alon Barad
4 views•10 min read