@@ -41,12 +41,25 @@ def _redirect_safe(self, url, default=None):
41
41
# \ is not valid in urls, but some browsers treat it as /
42
42
# instead of %5C, causing `\\` to behave as `//`
43
43
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 ('/' )} "
44
54
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 ):
46
59
# require that next_url be absolute path within our path
47
60
allow = False
48
61
# OR pass our cross-origin check
49
- if parsed .netloc :
62
+ if parsed .scheme or parsed . netloc :
50
63
# if full URL, run our cross-origin check:
51
64
origin = f"{ parsed .scheme } ://{ parsed .netloc } "
52
65
origin = origin .lower ()
0 commit comments