From 47cf968c125e3fab317e10fe150ec479e745f995 Mon Sep 17 00:00:00 2001
From: Jake Howard <git@theorangeone.net>
Date: Wed, 1 Apr 2026 18:21:30 +0200
Subject: [PATCH] [5.2.x] Fixed CVE-2026-35192 -- Ensured Vary header is sent
 when setting session cookie with SESSION_SAVE_EVERY_REQUEST=True.

Thank you Jacob Walls and Natalia Bidart for reviews.

Backport of 7f6e9b55130d5158804c0acbc0b24ccb7422ed82 from main.
---
 django/contrib/sessions/middleware.py | 11 ++++++++---
 docs/releases/5.2.14.txt              | 11 +++++++++++
 tests/sessions_tests/tests.py         | 28 +++++++++++++++++++++++++++
 3 files changed, 47 insertions(+), 3 deletions(-)

diff --git a/django/contrib/sessions/middleware.py b/django/contrib/sessions/middleware.py
index 9c934f9dddab..c6a5e336788c 100644
--- a/django/contrib/sessions/middleware.py
+++ b/django/contrib/sessions/middleware.py
@@ -40,10 +40,11 @@ def process_response(self, request, response):
                 domain=settings.SESSION_COOKIE_DOMAIN,
                 samesite=settings.SESSION_COOKIE_SAMESITE,
             )
-            patch_vary_headers(response, ("Cookie",))
+            need_vary_cookie = True
         else:
-            if accessed:
-                patch_vary_headers(response, ("Cookie",))
+            # If the session was accessed, it must be varied on, regardless of
+            # whether it was modified or will be saved.
+            need_vary_cookie = accessed
             if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
                 if request.session.get_expire_at_browser_close():
                     max_age = None
@@ -74,4 +75,8 @@ def process_response(self, request, response):
                         httponly=settings.SESSION_COOKIE_HTTPONLY or None,
                         samesite=settings.SESSION_COOKIE_SAMESITE,
                     )
+                    # With a session cookie set, it must be varied on.
+                    need_vary_cookie = True
+        if need_vary_cookie:
+            patch_vary_headers(response, ("Cookie",))
         return response
diff --git a/tests/sessions_tests/tests.py b/tests/sessions_tests/tests.py
index 9eabb933a8ab..4d09b86c9df6 100644
--- a/tests/sessions_tests/tests.py
+++ b/tests/sessions_tests/tests.py
@@ -1021,6 +1021,7 @@ def test_secure_session_cookie(self):
         # Handle the response through the middleware
         response = middleware(request)
         self.assertIs(response.cookies[settings.SESSION_COOKIE_NAME]["secure"], True)
+        self.assertEqual(response.headers["Vary"], "Cookie")
 
     @override_settings(SESSION_COOKIE_HTTPONLY=True)
     def test_httponly_session_cookie(self):
@@ -1161,6 +1162,7 @@ def response_ending_session(request):
             ),
             str(response.cookies[settings.SESSION_COOKIE_NAME]),
         )
+        self.assertEqual(response.headers["Vary"], "Cookie")
 
     def test_flush_empty_without_session_cookie_doesnt_set_cookie(self):
         def response_ending_session(request):
@@ -1178,6 +1180,32 @@ def response_ending_session(request):
         # The session is accessed so "Vary: Cookie" should be set.
         self.assertEqual(response.headers["Vary"], "Cookie")
 
+    @override_settings(SESSION_SAVE_EVERY_REQUEST=True)
+    def test_save_every_request_with_non_empty_session_renews_session_cookie(self):
+        request = self.request_factory.get("/")
+        middleware = SessionMiddleware(self.get_response_touching_session)
+
+        # Make sure the request has a session.
+        middleware(request)
+
+        # A cookie should be set.
+        self.assertIs(request.session.is_empty(), False)
+        self.assertEqual(request.session["hello"], "world")
+
+        request.COOKIES[settings.SESSION_COOKIE_NAME] = request.session.session_key
+
+        def simple_view(request):
+            return HttpResponse("Session test")
+
+        middleware = SessionMiddleware(simple_view)
+        response = middleware(request)
+
+        # A cookie should be set because SESSION_SAVE_EVERY_REQUEST=True,
+        # even though the session wasn't touched.
+        self.assertIn(settings.SESSION_COOKIE_NAME, response.cookies)
+        # There's a session, so also Vary on it.
+        self.assertEqual(response.headers["Vary"], "Cookie")
+
     def test_empty_session_saved(self):
         """
         If a session is emptied of data but still has a key, it should still
