Prevent open redirect via backslash in path (#2680)

asgi_send_redirect() only collapsed leading forward slashes, so a path
like /\example.com/ produced a Location of /\example.com. Browsers
normalise backslashes to forward slashes, turning that into the
protocol-relative //example.com and redirecting off-site. Collapse any
run of leading slashes and backslashes to a single slash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2026-06-10 23:17:16 -07:00
commit 1c514d69f6
2 changed files with 26 additions and 3 deletions

View file

@ -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 down to a single slash. //example.com is a protocol-relative
# URL, and browsers normalise backslashes to slashes so /\example.com would
# be treated as //example.com - https://github.com/simonw/datasette/issues/2680
location = re.sub(r"^[/\\]+", "/", location)
await asgi_send(
send,
"",

View file

@ -104,3 +104,24 @@ def test_custom_route_pattern_with_slash_slash_302(custom_pages_client):
response = custom_pages_client.get("//example.com/")
assert response.status == 302
assert response.headers["location"] == "/example.com"
@pytest.mark.parametrize(
"path",
(
"/\\example.com/",
"/\\\\example.com/",
"/\\/example.com/",
),
)
def test_redirect_does_not_allow_backslash_open_redirect(custom_pages_client, path):
# https://github.com/simonw/datasette/issues/2680
# Browsers normalise backslashes to forward slashes, so a Location of
# /\example.com would be treated as the protocol-relative //example.com
response = custom_pages_client.get(path)
assert response.status == 302
location = response.headers["location"]
assert location == "/example.com"
# Must not start with anything a browser reads as protocol-relative
assert not location.startswith("//")
assert not location.startswith("/\\")