Repository: Kludex/starlette (python)

Issue:
CORSMiddleware does not provide explicit origin although Authorization header is present

### Discussed in https://github.com/encode/starlette/discussions/1823

<div type='discussions-op-text'>

<sup>Originally posted by **gyusang** August 26, 2022</sup>
When sending a CORS request with credentials, wildcard origin is rejected by the standard.
The CORS middleware handles this case when cookies are included, but is missing the case when `Authorization` header is present.
https://github.com/encode/starlette/blob/31164e346b9bd1ce17d968e1301c3bb2c23bb418/starlette/middleware/cors.py#L164-L165
Since Token authentication is also widely used these days, I believe explicit header should be returned when `Authorization` header is present.</div>

<!-- POLAR PLEDGE BADGE START -->
> [!IMPORTANT]
> - We're using [Polar.sh](https://polar.sh/encode) so you can upvote and help fund this issue.
> - We receive the funding once the issue is completed & confirmed by you.
> - Thank you in advance for helping prioritize & fund our backlog.

<a href="https://polar.sh/encode/starlette/issues/1832">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/encode/starlette/issues/1832/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/encode/starlette/issues/1832/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Patch:
diff --git a/starlette/middleware/cors.py b/starlette/middleware/cors.py
index 36ba9f93f..b65fc15a0 100644
--- a/starlette/middleware/cors.py
+++ b/starlette/middleware/cors.py
@@ -68,6 +68,7 @@ def __init__(
         self.allow_headers = [h.lower() for h in allow_headers]
         self.allow_all_origins = allow_all_origins
         self.allow_all_headers = allow_all_headers
+        self.allow_credentials = allow_credentials
         self.preflight_explicit_allow_origin = preflight_explicit_allow_origin
         self.allow_origin_regex = compiled_allow_origin_regex
         self.allow_private_network = allow_private_network
@@ -161,15 +162,12 @@ async def send(self, message: Message, send: Send, request_headers: Headers) ->
         headers = MutableHeaders(scope=message)
         headers.update(self.simple_headers)
         origin = request_headers["Origin"]
-        has_cookie = "cookie" in request_headers
 
-        # If request includes any cookie headers, then we must respond
-        # with the specific origin instead of '*'.
-        if self.allow_all_origins and has_cookie:
+        # If credentials are allowed, then we must respond with the specific origin instead of '*'.
+        if self.allow_all_origins and self.allow_credentials:
             self.allow_explicit_origin(headers, origin)
 
-        # If we only allow specific origins, then we have to mirror back
-        # the Origin header in the response.
+        # If we only allow specific origins, then we have to mirror back the Origin header in the response.
         elif not self.allow_all_origins and self.is_allowed_origin(origin=origin):
             self.allow_explicit_origin(headers, origin)
 


Test patch:
diff --git a/tests/middleware/test_cors.py b/tests/middleware/test_cors.py
index cbee7d6e7..7fb7d0bef 100644
--- a/tests/middleware/test_cors.py
+++ b/tests/middleware/test_cors.py
@@ -48,7 +48,7 @@ def homepage(request: Request) -> PlainTextResponse:
     response = client.get("/", headers=headers)
     assert response.status_code == 200
     assert response.text == "Homepage"
-    assert response.headers["access-control-allow-origin"] == "*"
+    assert response.headers["access-control-allow-origin"] == "https://example.org"
     assert response.headers["access-control-expose-headers"] == "X-Status"
     assert response.headers["access-control-allow-credentials"] == "true"
 
@@ -404,30 +404,7 @@ def homepage(request: Request) -> PlainTextResponse:
     assert "access-control-allow-origin" not in response.headers
 
 
-def test_cors_credentialed_requests_return_specific_origin(
-    test_client_factory: TestClientFactory,
-) -> None:
-    def homepage(request: Request) -> PlainTextResponse:
-        return PlainTextResponse("Homepage", status_code=200)
-
-    app = Starlette(
-        routes=[Route("/", endpoint=homepage)],
-        middleware=[Middleware(CORSMiddleware, allow_origins=["*"])],
-    )
-    client = test_client_factory(app)
-
-    # Test credentialed request
-    headers = {"Origin": "https://example.org", "Cookie": "star_cookie=sugar"}
-    response = client.get("/", headers=headers)
-    assert response.status_code == 200
-    assert response.text == "Homepage"
-    assert response.headers["access-control-allow-origin"] == "https://example.org"
-    assert "access-control-allow-credentials" not in response.headers
-
-
-def test_cors_vary_header_defaults_to_origin(
-    test_client_factory: TestClientFactory,
-) -> None:
+def test_cors_vary_header_defaults_to_origin(test_client_factory: TestClientFactory) -> None:
     def homepage(request: Request) -> PlainTextResponse:
         return PlainTextResponse("Homepage", status_code=200)
 
@@ -445,9 +422,7 @@ def homepage(request: Request) -> PlainTextResponse:
     assert response.headers["vary"] == "Origin"
 
 
-def test_cors_vary_header_is_not_set_for_non_credentialed_request(
-    test_client_factory: TestClientFactory,
-) -> None:
+def test_cors_vary_header_is_not_set_for_non_credentialed_request(test_client_factory: TestClientFactory) -> None:
     def homepage(request: Request) -> PlainTextResponse:
         return PlainTextResponse("Homepage", status_code=200, headers={"Vary": "Accept-Encoding"})
 
@@ -462,19 +437,17 @@ def homepage(request: Request) -> PlainTextResponse:
     assert response.headers["vary"] == "Accept-Encoding"
 
 
-def test_cors_vary_header_is_properly_set_for_credentialed_request(
-    test_client_factory: TestClientFactory,
-) -> None:
+def test_cors_vary_header_is_properly_set_for_credentialed_request(test_client_factory: TestClientFactory) -> None:
     def homepage(request: Request) -> PlainTextResponse:
         return PlainTextResponse("Homepage", status_code=200, headers={"Vary": "Accept-Encoding"})
 
     app = Starlette(
         routes=[Route("/", endpoint=homepage)],
-        middleware=[Middleware(CORSMiddleware, allow_origins=["*"])],
+        middleware=[Middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True)],
     )
     client = test_client_factory(app)
 
-    response = client.get("/", headers={"Cookie": "foo=bar", "Origin": "https://someplace.org"})
+    response = client.get("/", headers={"Origin": "https://someplace.org"})
     assert response.status_code == 200
     assert response.headers["vary"] == "Accept-Encoding, Origin"
 
@@ -498,38 +471,25 @@ def homepage(request: Request) -> PlainTextResponse:
     assert response.headers["vary"] == "Accept-Encoding, Origin"
 
 
-def test_cors_allowed_origin_does_not_leak_between_credentialed_requests(
-    test_client_factory: TestClientFactory,
-) -> None:
+def test_cors_allowed_origin_does_not_leak_between_requests(test_client_factory: TestClientFactory) -> None:
     def homepage(request: Request) -> PlainTextResponse:
         return PlainTextResponse("Homepage", status_code=200)
 
     app = Starlette(
-        routes=[
-            Route("/", endpoint=homepage),
-        ],
-        middleware=[
-            Middleware(
-                CORSMiddleware,
-                allow_origins=["*"],
-                allow_headers=["*"],
-                allow_methods=["*"],
-            )
-        ],
+        routes=[Route("/", endpoint=homepage)],
+        middleware=[Middleware(CORSMiddleware, allow_origins=["https://example.org"])],
     )
 
     client = test_client_factory(app)
-    response = client.get("/", headers={"Origin": "https://someplace.org"})
-    assert response.headers["access-control-allow-origin"] == "*"
-    assert "access-control-allow-credentials" not in response.headers
 
-    response = client.get("/", headers={"Cookie": "foo=bar", "Origin": "https://someplace.org"})
-    assert response.headers["access-control-allow-origin"] == "https://someplace.org"
-    assert "access-control-allow-credentials" not in response.headers
+    response = client.get("/", headers={"Origin": "https://example.org"})
+    assert response.headers["access-control-allow-origin"] == "https://example.org"
 
-    response = client.get("/", headers={"Origin": "https://someplace.org"})
-    assert response.headers["access-control-allow-origin"] == "*"
-    assert "access-control-allow-credentials" not in response.headers
+    response = client.get("/", headers={"Origin": "https://other.org"})
+    assert "access-control-allow-origin" not in response.headers
+
+    response = client.get("/", headers={"Origin": "https://example.org"})
+    assert response.headers["access-control-allow-origin"] == "https://example.org"
 
 
 def test_cors_private_network_access_allowed(test_client_factory: TestClientFactory) -> None: