From 6fdd82992c8b2151b662db83a909f67270096ba8 Mon Sep 17 00:00:00 2001 From: adilburaksen Date: Sat, 16 May 2026 21:24:27 +0300 Subject: [PATCH 1/2] handlers: reject protocol-relative URLs in redirect validation _SAFE_URL_PATTERN's relative-URL branch matched the empty string prefix of '//evil.com', so check_redirect_url accepted protocol-relative URLs as safe. Browsers interpret '//evil.com/path' as 'same-scheme://evil.com/path', enabling open redirect from any endpoint that passes the user-supplied 'next' / 'redirect' parameter through check_redirect_url. Add a negative lookahead (?!//) before the relative-URL branch so that any URL beginning with '//' is rejected unless it already matched the explicit-scheme branch (http://, https://, etc). --- src/appengine/handlers/base_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/appengine/handlers/base_handler.py b/src/appengine/handlers/base_handler.py index dc69a51e5aa..d054ee08034 100644 --- a/src/appengine/handlers/base_handler.py +++ b/src/appengine/handlers/base_handler.py @@ -45,7 +45,7 @@ # https://github.com/google/closure-library/blob/ # 3037e09cc471bfe99cb8f0ee22d9366583a20c28/closure/goog/html/safeurl.js _SAFE_URL_PATTERN = re.compile( - r'^(?:(?:https?|mailto|ftp):|[^:/?#]*(?:[/?#]|$))', flags=re.IGNORECASE) + r'^(?:(?:https?|mailto|ftp):|(?!//)[^:/?#]*(?:[/?#]|$))', flags=re.IGNORECASE) def add_jinja2_filter(name, fn): From b188f3e0376c08c0dff34bc789975495d5030cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adil=20Burak=20=C5=9Een?= <56400880+adilburaksen@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:51:58 +0300 Subject: [PATCH 2/2] Reject backslash and control/whitespace bypasses in redirect validation --- src/appengine/handlers/base_handler.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/appengine/handlers/base_handler.py b/src/appengine/handlers/base_handler.py index d054ee08034..17df76f7ba7 100644 --- a/src/appengine/handlers/base_handler.py +++ b/src/appengine/handlers/base_handler.py @@ -130,6 +130,15 @@ def make_logout_url(dest_url): def check_redirect_url(url): """Check redirect URL is safe.""" + # Browsers strip ASCII tab/newline and leading control/whitespace characters + # from URLs and normalize backslashes to forward slashes, so inputs such as + # `/\\evil.com`, ` //evil.com` or `\r\n//evil.com` would otherwise pass the + # scheme-relative check below and then navigate to an external host. Reject + # any input with a backslash, a control character, or leading/trailing + # whitespace before validating. + if (not url or url != url.strip() or '\\' in url + or any(ord(char) < 0x20 for char in url)): + raise helpers.EarlyExitError('Invalid redirect.', 403) if not _SAFE_URL_PATTERN.match(url): raise helpers.EarlyExitError('Invalid redirect.', 403)