diff --git a/datasette/app.py b/datasette/app.py index be507241..09936b3a 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -2150,11 +2150,6 @@ class DatasetteRouter: context = {} if path.endswith(b"/"): path = path.rstrip(b"/") - - # If you redirect with a // at the beginning, you end up with an open redirect, so - # https://my.site//foo/ - will redirect to https://foo - path = re.sub(rb"^/+", b"/", path) - if request.scope["query_string"]: path += b"?" + request.scope["query_string"] await asgi_send_redirect(send, path.decode("latin1")) diff --git a/datasette/utils/asgi.py b/datasette/utils/asgi.py index 40214cbe..7f3329a6 100644 --- a/datasette/utils/asgi.py +++ b/datasette/utils/asgi.py @@ -6,6 +6,7 @@ from pathlib import Path from http.cookies import SimpleCookie, Morsel import aiofiles import aiofiles.os +import re # Workaround for adding samesite support to pre 3.8 python Morsel._reserved["samesite"] = "SameSite" @@ -248,6 +249,9 @@ 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) await asgi_send( send, "", diff --git a/tests/test_custom_pages.py b/tests/test_custom_pages.py index ccc139ce..39a4c06b 100644 --- a/tests/test_custom_pages.py +++ b/tests/test_custom_pages.py @@ -100,6 +100,7 @@ def test_custom_route_pattern_404(custom_pages_client): def test_custom_route_pattern_with_slash_slash_302(custom_pages_client): - response = custom_pages_client.get("//nastyOpenRedirect/") + # https://github.com/simonw/datasette/issues/2429 + response = custom_pages_client.get("//example.com/") assert response.status == 302 - assert response.headers["location"] == "/nastyOpenRedirect" + assert response.headers["location"] == "/example.com"