From fd592a5357255dd8af76f8f63ab036e996caf631 Mon Sep 17 00:00:00 2001 From: B58TU Date: Tue, 22 Sep 2026 22:13:17 -0700 Subject: [PATCH] Reject protocol-relative URLs in post-login redirect --- app/controllers/login.py | 8 ++++++-- tests/test_controller_edge_cases.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/controllers/login.py b/app/controllers/login.py index 265918f..8aafaf7 100644 --- a/app/controllers/login.py +++ b/app/controllers/login.py @@ -90,9 +90,13 @@ def login_post(): flash('Login successful!', FLASH_SUCCESS) # Retrieve and clear the redirect from session - # Only allow relative URLs to prevent redirecting to external sites + # Only allow relative URLs to prevent redirecting to external sites. + # '//evil.com' and '\\evil.com' also start with '/', and browsers + # happily resolve them as other hosts. wonderful spec behavior. redirect_url = session.pop('login_redirect', '/') - if not redirect_url.startswith('/'): + if (not redirect_url.startswith('/') + or redirect_url.startswith('//') + or '\\' in redirect_url): redirect_url = '/' return redirect(redirect_url) else: diff --git a/tests/test_controller_edge_cases.py b/tests/test_controller_edge_cases.py index 3154c95..ed42769 100644 --- a/tests/test_controller_edge_cases.py +++ b/tests/test_controller_edge_cases.py @@ -24,6 +24,18 @@ def test_login_does_not_redirect_to_external_referrer(client, alice): assert response.location == '/' +@pytest.mark.parametrize('referrer', [ + 'http://localhost//evil.example', + 'http://localhost/\\evil.example', +]) +def test_login_does_not_redirect_to_protocol_relative_referrer(client, alice, referrer): + client.get('/login', headers={'Referer': referrer}) + response = client.post('/login', data={ + 'name': 'alice', 'password': 'alice-password', + }) + assert response.location == '/' + + def test_login_missing_and_invalid_name(client): missing = client.post('/login', data={'name': '', 'password': ''}) invalid = client.post('/login', data={'name': '