Repository: streamlit/streamlit
Issue category: Security Vulnerability

Undesired host redirect with trailing slash and host path 
### Checklist

- [X] I have searched the [existing issues](https://github.com/streamlit/streamlit/issues) for similar issues.
- [X] I added a very descriptive title to this issue.
- [X] I have provided sufficient information below to help reproduce this issue.

### Summary

If a "hostname" path is provided with a trailing slash, streamlit will respond with a redirect to the host in the path.
Example url (streamlit running locally):  `http://localhost:8501//authorization.site/`
This will 301 redirect to `authorization.site`
This is undesired behavior and can lead to phishing attacks when an trusted hostname redirects to a malicious page.

This was introduced in the [1.39.0 release](https://github.com/streamlit/streamlit/releases/tag/1.39.0) by https://github.com/streamlit/streamlit/pull/9500



### Reproducible Code Example

_No response_

### Steps To Reproduce

1. Request a path against a streamlit server version 1.39.0 with the format `{server_hostname}//{redirect_hostname}/`, eg (http://localhost:8501//authorization.site/)
2. Observe the redirect to the redirect_hostname

### Expected Behavior

Expected behavior is for the server to return a 403 Forbidden response, and this was the behavior in streamlit release version `1.38.0`
Example Request for Streamlit 1.38.0:
```
curl -v http://localhost:8080//authorization.site/
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET //authorization.site/ HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 403 Forbidden
< Server: TornadoServer/6.4.1
< Content-Type: text/html; charset=UTF-8
< Date: Sun, 20 Oct 2024 17:49:24 GMT
< Content-Length: 69
< Vary: Accept-Encoding
< 
* Connection #0 to host localhost left intact
<html><title>403: Forbidden</title><body>403: Forbidden</body></html>%  
```

### Current Behavior

Curl response when requesting against Streamlit version 1.39.0:
```
curl -v http://localhost:8080//authorization.site/
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET //authorization.site/ HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 301 Moved Permanently
< Server: TornadoServer/6.4.1
< Content-Type: text/html; charset=UTF-8
< Date: Sun, 20 Oct 2024 17:30:50 GMT
< Location: //authorization.site
< Content-Length: 0
< Vary: Accept-Encoding
< 
* Connection #0 to host localhost left intact
 ```

### Is this a regression?

- [X] Yes, this used to work in a previous version.

### Debug info

- Streamlit version: 1.39.0
- Python version: 3.9
- Operating System: MacOs 
- Browser: Curl


### Additional Information

_No response_


Functions to edit:
lib/streamlit/web/server/server.py:Server._create_app

Patch:
diff --git a/lib/streamlit/web/server/server.py b/lib/streamlit/web/server/server.py
index 027db4ab064a..5b3d71f4571f 100644
--- a/lib/streamlit/web/server/server.py
+++ b/lib/streamlit/web/server/server.py
@@ -369,7 +369,12 @@ def _create_app(self) -> tornado.web.Application:
             routes.extend(
                 [
                     (
-                        make_url_path_regex(base, "(.*)", trailing_slash="required"),
+                        # We want to remove paths with a trailing slash, but if the path
+                        # starts with a double slash //, the redirect will point
+                        # the browser to the wrong host.
+                        make_url_path_regex(
+                            base, "(?!/)(.*)", trailing_slash="required"
+                        ),
                         RemoveSlashHandler,
                     ),
                     (

Test patch: diff --git a/lib/tests/streamlit/web/server/routes_test.py b/lib/tests/streamlit/web/server/routes_test.py
index a4f7a62781fe..b47c678c16a7 100644
--- a/lib/tests/streamlit/web/server/routes_test.py
+++ b/lib/tests/streamlit/web/server/routes_test.py
@@ -196,7 +196,7 @@ def get_app(self):
         return tornado.web.Application(
             [
                 (
-                    r"/(.*)/",
+                    r"^/(?!/)(.*)",
                     RemoveSlashHandler,
                 )
             ]
@@ -210,6 +210,13 @@ def test_parse_url_path_301(self):
             assert r.code == 301
             assert r.headers["Location"] == paths[idx].rstrip("/")
 
+    def test_parse_url_path_404(self):
+        paths = ["//page1/", "//page2/page3/"]
+        responses = [self.fetch(path, follow_redirects=False) for path in paths]
+
+        for r in responses:
+            assert r.code == 404
+
 
 class AddSlashHandlerTest(tornado.testing.AsyncHTTPTestCase):
     def get_app(self):