Skip to content

Commit 47cf968

Browse files
RealOrangeOnesarahboyce
authored andcommitted
[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 7f6e9b5 from main.
1 parent 2ec27ed commit 47cf968

3 files changed

Lines changed: 47 additions & 3 deletions

File tree

django/contrib/sessions/middleware.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ def process_response(self, request, response):
4040
domain=settings.SESSION_COOKIE_DOMAIN,
4141
samesite=settings.SESSION_COOKIE_SAMESITE,
4242
)
43-
patch_vary_headers(response, ("Cookie",))
43+
need_vary_cookie = True
4444
else:
45-
if accessed:
46-
patch_vary_headers(response, ("Cookie",))
45+
# If the session was accessed, it must be varied on, regardless of
46+
# whether it was modified or will be saved.
47+
need_vary_cookie = accessed
4748
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
4849
if request.session.get_expire_at_browser_close():
4950
max_age = None
@@ -74,4 +75,8 @@ def process_response(self, request, response):
7475
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
7576
samesite=settings.SESSION_COOKIE_SAMESITE,
7677
)
78+
# With a session cookie set, it must be varied on.
79+
need_vary_cookie = True
80+
if need_vary_cookie:
81+
patch_vary_headers(response, ("Cookie",))
7782
return response

docs/releases/5.2.14.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,14 @@ relying on :setting:`FILE_UPLOAD_MAX_MEMORY_SIZE`.
2020

2121
This issue has severity "low" according to the :ref:`Django security policy
2222
<security-disclosure>`.
23+
24+
CVE-2026-35192: Session fixation via public cached pages and ``SESSION_SAVE_EVERY_REQUEST``
25+
===========================================================================================
26+
27+
Response headers did not :ref:`vary on <using-vary-headers>` cookies if a
28+
session was not modified, but :setting:`SESSION_SAVE_EVERY_REQUEST` was
29+
``True``. A remote attacker could steal a user's session after that user visits
30+
a cached public page.
31+
32+
This issue has severity "low" according to the :ref:`Django security policy
33+
<security-disclosure>`.

tests/sessions_tests/tests.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,7 @@ def test_secure_session_cookie(self):
10211021
# Handle the response through the middleware
10221022
response = middleware(request)
10231023
self.assertIs(response.cookies[settings.SESSION_COOKIE_NAME]["secure"], True)
1024+
self.assertEqual(response.headers["Vary"], "Cookie")
10241025

10251026
@override_settings(SESSION_COOKIE_HTTPONLY=True)
10261027
def test_httponly_session_cookie(self):
@@ -1161,6 +1162,7 @@ def response_ending_session(request):
11611162
),
11621163
str(response.cookies[settings.SESSION_COOKIE_NAME]),
11631164
)
1165+
self.assertEqual(response.headers["Vary"], "Cookie")
11641166

11651167
def test_flush_empty_without_session_cookie_doesnt_set_cookie(self):
11661168
def response_ending_session(request):
@@ -1178,6 +1180,32 @@ def response_ending_session(request):
11781180
# The session is accessed so "Vary: Cookie" should be set.
11791181
self.assertEqual(response.headers["Vary"], "Cookie")
11801182

1183+
@override_settings(SESSION_SAVE_EVERY_REQUEST=True)
1184+
def test_save_every_request_with_non_empty_session_renews_session_cookie(self):
1185+
request = self.request_factory.get("/")
1186+
middleware = SessionMiddleware(self.get_response_touching_session)
1187+
1188+
# Make sure the request has a session.
1189+
middleware(request)
1190+
1191+
# A cookie should be set.
1192+
self.assertIs(request.session.is_empty(), False)
1193+
self.assertEqual(request.session["hello"], "world")
1194+
1195+
request.COOKIES[settings.SESSION_COOKIE_NAME] = request.session.session_key
1196+
1197+
def simple_view(request):
1198+
return HttpResponse("Session test")
1199+
1200+
middleware = SessionMiddleware(simple_view)
1201+
response = middleware(request)
1202+
1203+
# A cookie should be set because SESSION_SAVE_EVERY_REQUEST=True,
1204+
# even though the session wasn't touched.
1205+
self.assertIn(settings.SESSION_COOKIE_NAME, response.cookies)
1206+
# There's a session, so also Vary on it.
1207+
self.assertEqual(response.headers["Vary"], "Cookie")
1208+
11811209
def test_empty_session_saved(self):
11821210
"""
11831211
If a session is emptied of data but still has a key, it should still

0 commit comments

Comments
 (0)