From 9fbd31df00cacddaa510c2ce5dad29712d6ef121 Mon Sep 17 00:00:00 2001 From: ATOM00blue <219721791+ATOM00blue@users.noreply.github.com> Date: Fri, 22 May 2026 06:34:22 +0530 Subject: [PATCH] Block open redirect via backslash in redirect Location The leading-slash collapse added for #2429 only stripped forward slashes, so a path like /\example.com/ (or its %5C-encoded form) was redirected to Location: /\example.com. Browsers treat a backslash as a slash, so that resolves to //example.com and then https://example.com, re-opening the open redirect. Collapse leading backslashes as well as slashes. Extends the existing slash-slash test to cover /\example.com/ and /\/example.com/. Fixes #2680 --- datasette/utils/asgi.py | 8 +++++--- tests/test_custom_pages.py | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/datasette/utils/asgi.py b/datasette/utils/asgi.py index 35f243b6..c68f1a27 100644 --- a/datasette/utils/asgi.py +++ b/datasette/utils/asgi.py @@ -330,9 +330,11 @@ async def asgi_send_html(send, html, status=200, headers=None): async def asgi_send_redirect(send, location, status=302): - # Prevent open redirect vulnerability: strip multiple leading slashes - # //example.com would be interpreted as a protocol-relative URL (e.g., https://example.com/) - location = re.sub(r"^/+", "/", location) + # Prevent open redirect vulnerability: collapse leading slashes and + # backslashes into a single slash. Browsers treat a backslash as a slash, + # so //example.com, /\example.com and /\/example.com would all otherwise be + # interpreted as a protocol-relative URL (e.g. https://example.com/). + location = re.sub(r"^[/\\]+", "/", location) await asgi_send( send, "", diff --git a/tests/test_custom_pages.py b/tests/test_custom_pages.py index 39a4c06b..3fadb587 100644 --- a/tests/test_custom_pages.py +++ b/tests/test_custom_pages.py @@ -99,8 +99,18 @@ def test_custom_route_pattern_404(custom_pages_client): assert ">Oh no