@@ -41,12 +41,25 @@ def _redirect_safe(self, url, default=None):
4141 # \ is not valid in urls, but some browsers treat it as /
4242 # instead of %5C, causing `\\` to behave as `//`
4343 url = url .replace ("\\ " , "%5C" )
44+ # urllib and browsers interpret extra '/' in the scheme separator (`scheme:///host/path`)
45+ # differently.
46+ # urllib gives scheme=scheme, netloc='', path='/host/path', while
47+ # browsers get scheme=scheme, netloc='host', path='/path'
48+ # so make sure ':///*' collapses to '://' by splitting and stripping any additional leading slash
49+ # don't allow any kind of `:/` shenanigans by splitting on ':' only
50+ # and replacing `:/*` with exactly `://`
51+ if ":" in url :
52+ scheme , _ , rest = url .partition (":" )
53+ url = f"{ scheme } ://{ rest .lstrip ('/' )} "
4454 parsed = urlparse (url )
45- if parsed .netloc or not (parsed .path + "/" ).startswith (self .base_url ):
55+ # full url may be `//host/path` (empty scheme == same scheme as request)
56+ # or `https://host/path`
57+ # or even `https:///host/path` (invalid, but accepted and ambiguously interpreted)
58+ if (parsed .scheme or parsed .netloc ) or not (parsed .path + "/" ).startswith (self .base_url ):
4659 # require that next_url be absolute path within our path
4760 allow = False
4861 # OR pass our cross-origin check
49- if parsed .netloc :
62+ if parsed .scheme or parsed . netloc :
5063 # if full URL, run our cross-origin check:
5164 origin = f"{ parsed .scheme } ://{ parsed .netloc } "
5265 origin = origin .lower ()
0 commit comments