diff --git a/blacksheep/server/cors.py b/blacksheep/server/cors.py index c9150570..20c3e9c0 100644 --- a/blacksheep/server/cors.py +++ b/blacksheep/server/cors.py @@ -289,7 +289,14 @@ async def cors_middleware(request: Request, handler): return _get_invalid_origin_response() # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin - origin_response = b"*" if "*" in policy.allow_origins else origin + # A literal "*" is invalid alongside credentials per the Fetch spec - browsers + # reject the response outright - so the specific request origin must be echoed + # back instead whenever credentials are allowed. + origin_response = ( + b"*" + if "*" in policy.allow_origins and not policy.allow_credentials + else origin + ) if next_request_method: # This is a preflight request; diff --git a/tests/test_cors.py b/tests/test_cors.py index 5a0981a7..ddc9c7e6 100644 --- a/tests/test_cors.py +++ b/tests/test_cors.py @@ -473,6 +473,66 @@ async def post_example(): ... assert response.headers.get_single(b"Access-Control-Allow-Credentials") == b"true" +async def test_cors_preflight_request_allow_any_origin_with_credentials(app): + """ + A literal "*" can't be combined with credentials - browsers reject the response + outright (Fetch spec) - so when both are configured, the request's own Origin must + be echoed back instead of "*", on both preflight and actual responses. + """ + app.use_cors( + allow_methods="GET POST", + allow_origins="*", + allow_credentials=True, + ) + + @app.router.get("/") + async def home(): + return text("Hello, World") + + @app.router.post("/") + async def post_example(): ... + + await app.start() + + await app( + get_example_scope( + "OPTIONS", + "/", + [ + (b"Origin", b"https://www.neoteroi.dev"), + (b"Access-Control-Request-Method", b"POST"), + ], + ), + MockReceive(), + MockSend(), + ) + + response = app.response + assert response.status == 200 + assert ( + response.headers.get_single(b"Access-Control-Allow-Origin") + == b"https://www.neoteroi.dev" + ) + assert response.headers.get_single(b"Access-Control-Allow-Credentials") == b"true" + + await app( + get_example_scope( + "GET", + "/", + [(b"Origin", b"https://www.neoteroi.dev")], + ), + MockReceive(), + MockSend(), + ) + + response = app.response + assert ( + response.headers.get_single(b"Access-Control-Allow-Origin") + == b"https://www.neoteroi.dev" + ) + assert response.headers.get_single(b"Access-Control-Allow-Credentials") == b"true" + + async def test_cors_preflight_request_allow_any(app): app.use_cors(allow_methods="*", allow_origins="*", allow_headers="*")