Repository: litestar-org/litestar

Issue:
Bug: Server-side session middleware overrides CSRF cookies
### Description

I noticed that it seems applying both a ServerSideSession and CSRF protection, breaks the CSRF middleware. 

I have recently been digging into the session features and noticed this. Using no or client Cookie side session middleware does not affect the CSRF middleware. On a normal get, the token gets set and csrf protection will be enabled. 

However, slotting in ServerSideSession, which I'm using a redis store for, causes the csrftoken to never be set. No GET or HEAD request ever gets a set-cookie response. I haven't seen this behavior with any other middleware, whether its custom or any of the other builtins. 

I was looking through the code and noticed that on line 122 of litstar.middleware.session.server_side.py, it is using a headers['Set-Cookie'] instead of a headers.add. Causing it to override the set-cookie from the csrf. Im sure this could cause other issues as well. 

If you change this to a headesr.add, it fixes the issue and both middlewares work properly. 

### URL to code causing the issue

_No response_

### MCVE

```python
from litestar import Litestar, get, post
from litestar.config.csrf import CSRFConfig
from litestar.middleware.session.server_side import ServerSideSessionConfig

@get('/')
async def index() -> dict[str, str]:
    return {'status': "success"}
@post('/')
async def index_post() -> dict[str, str]:
    return {'status': "success"}


app = Litestar(
    route_handlers=[index, index_post],
    csrf_config=CSRFConfig(
        secret="Change_Me",
        cookie_name="x-csrftoken",
    ),
    middleware=[
        ServerSideSessionConfig(
            renew_on_access=True,
            max_age=30*60
        ).middleware
    ]
)
```


### Steps to reproduce

```bash
1. Make a GET request to the app
2. Observe that a cookie is set for session, but not for the csrf
3. Change line 122 of litestar.middleware.session.server_side.py to a headers.add
4. Remake the GET request to the app
5. Observe both cookies are now set
6. Make a POST request and observe the app functions as expected
```


### Screenshots

_No response_

### Logs

_No response_

### Litestar Version

I was using 2.0.0b2 when I noticed it, however it is also present in 2.0.0b3

### Platform

- [ ] Linux
- [X] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
## Funding
* If you would like to see an issue prioritized, make a pledge towards it!
* We receive the pledge once the issue is completed & verified

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

Bug: Server-side session middleware overrides CSRF cookies
### Description

I noticed that it seems applying both a ServerSideSession and CSRF protection, breaks the CSRF middleware. 

I have recently been digging into the session features and noticed this. Using no or client Cookie side session middleware does not affect the CSRF middleware. On a normal get, the token gets set and csrf protection will be enabled. 

However, slotting in ServerSideSession, which I'm using a redis store for, causes the csrftoken to never be set. No GET or HEAD request ever gets a set-cookie response. I haven't seen this behavior with any other middleware, whether its custom or any of the other builtins. 

I was looking through the code and noticed that on line 122 of litstar.middleware.session.server_side.py, it is using a headers['Set-Cookie'] instead of a headers.add. Causing it to override the set-cookie from the csrf. Im sure this could cause other issues as well. 

If you change this to a headesr.add, it fixes the issue and both middlewares work properly. 

### URL to code causing the issue

_No response_

### MCVE

```python
from litestar import Litestar, get, post
from litestar.config.csrf import CSRFConfig
from litestar.middleware.session.server_side import ServerSideSessionConfig

@get('/')
async def index() -> dict[str, str]:
    return {'status': "success"}
@post('/')
async def index_post() -> dict[str, str]:
    return {'status': "success"}


app = Litestar(
    route_handlers=[index, index_post],
    csrf_config=CSRFConfig(
        secret="Change_Me",
        cookie_name="x-csrftoken",
    ),
    middleware=[
        ServerSideSessionConfig(
            renew_on_access=True,
            max_age=30*60
        ).middleware
    ]
)
```


### Steps to reproduce

```bash
1. Make a GET request to the app
2. Observe that a cookie is set for session, but not for the csrf
3. Change line 122 of litestar.middleware.session.server_side.py to a headers.add
4. Remake the GET request to the app
5. Observe both cookies are now set
6. Make a POST request and observe the app functions as expected
```


### Screenshots

_No response_

### Logs

_No response_

### Litestar Version

I was using 2.0.0b2 when I noticed it, however it is also present in 2.0.0b3

### Platform

- [ ] Linux
- [X] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
## Funding
* If you would like to see an issue prioritized, make a pledge towards it!
* We receive the pledge once the issue is completed & verified

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



Discussion:

Resolving patch:
diff --git a/litestar/middleware/session/server_side.py b/litestar/middleware/session/server_side.py
--- a/litestar/middleware/session/server_side.py
+++ b/litestar/middleware/session/server_side.py
@@ -119,7 +119,9 @@ async def store_in_message(self, scope_session: ScopeSession, message: Message,
         else:
             serialised_data = self.serialize_data(scope_session, scope)
             await self.set(session_id=session_id, data=serialised_data, store=store)
-            headers["Set-Cookie"] = Cookie(value=session_id, key=self.config.key, **cookie_params).to_header(header="")
+            headers.add(
+                "Set-Cookie", Cookie(value=session_id, key=self.config.key, **cookie_params).to_header(header="")
+            )
 
     async def load_from_connection(self, connection: ASGIConnection) -> dict[str, Any]:
         """Load session data from a connection and return it as a dictionary to be used in the current application


Test patch:
diff --git a/tests/unit/test_middleware/test_session/test_middleware.py b/tests/unit/test_middleware/test_session/test_middleware.py
--- a/tests/unit/test_middleware/test_session/test_middleware.py
+++ b/tests/unit/test_middleware/test_session/test_middleware.py
@@ -1,6 +1,6 @@
 from typing import TYPE_CHECKING, Dict, Optional
 
-from litestar import HttpMethod, Request, get, post, route
+from litestar import HttpMethod, Request, Response, get, post, route
 from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR
 from litestar.testing import create_test_client
 from litestar.types import Empty
@@ -146,3 +146,15 @@ def south_handler(request: Request) -> Dict[str, bool]:
 
         response = client.get("/south")
         assert response.json() == {"has_session": False}
+
+
+def test_does_not_override_cookies(session_backend_config_memory: "ServerSideSessionConfig") -> None:
+    # https://github.com/litestar-org/litestar/issues/2033
+
+    @get("/")
+    async def index() -> Response[str]:
+        return Response(cookies={"foo": "bar"}, content="hello")
+
+    with create_test_client(index, middleware=[session_backend_config_memory.middleware]) as client:
+        res = client.get("/")
+        assert res.cookies.get("foo") == "bar"