Skip to content

Commit 2f54853

Browse files
committed
[1.7.x] Fixed DoS possiblity in contrib.auth.views.logout()
Refs #20936 -- When logging out/ending a session, don't create a new, empty session. Previously, when logging out, the existing session was overwritten by a new sessionid instead of deleting the session altogether. This behavior added overhead by creating a new session record in whichever backend was in use: db, cache, etc. This extra session is unnecessary at the time since no session data is meant to be preserved when explicitly logging out. Backport of 393c0e2, 0885796, and 2dee853 from master Thanks Florian Apolloner and Carl Meyer for review. This is a security fix.
1 parent 95af894 commit 2f54853

File tree

7 files changed

+154
-27
lines changed

7 files changed

+154
-27
lines changed

django/contrib/sessions/backends/base.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ def clear(self):
142142
self.accessed = True
143143
self.modified = True
144144

145+
def is_empty(self):
146+
"Returns True when there is no session_key and the session is empty"
147+
try:
148+
return not bool(self._session_key) and not self._session_cache
149+
except AttributeError:
150+
return True
151+
145152
def _get_new_session_key(self):
146153
"Returns session key that isn't being used."
147154
while True:
@@ -268,7 +275,7 @@ def flush(self):
268275
"""
269276
self.clear()
270277
self.delete()
271-
self.create()
278+
self._session_key = None
272279

273280
def cycle_key(self):
274281
"""

django/contrib/sessions/backends/cached_db.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def flush(self):
7979
"""
8080
self.clear()
8181
self.delete(self.session_key)
82-
self.create()
82+
self._session_key = None
8383

8484

8585
# At bottom to avoid circular import

django/contrib/sessions/middleware.py

+29-21
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,40 @@ def process_request(self, request):
1818
def process_response(self, request, response):
1919
"""
2020
If request.session was modified, or if the configuration is to save the
21-
session every time, save the changes and set a session cookie.
21+
session every time, save the changes and set a session cookie or delete
22+
the session cookie if the session has been emptied.
2223
"""
2324
try:
2425
accessed = request.session.accessed
2526
modified = request.session.modified
27+
empty = request.session.is_empty()
2628
except AttributeError:
2729
pass
2830
else:
29-
if accessed:
30-
patch_vary_headers(response, ('Cookie',))
31-
if modified or settings.SESSION_SAVE_EVERY_REQUEST:
32-
if request.session.get_expire_at_browser_close():
33-
max_age = None
34-
expires = None
35-
else:
36-
max_age = request.session.get_expiry_age()
37-
expires_time = time.time() + max_age
38-
expires = cookie_date(expires_time)
39-
# Save the session data and refresh the client cookie.
40-
# Skip session save for 500 responses, refs #3881.
41-
if response.status_code != 500:
42-
request.session.save()
43-
response.set_cookie(settings.SESSION_COOKIE_NAME,
44-
request.session.session_key, max_age=max_age,
45-
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
46-
path=settings.SESSION_COOKIE_PATH,
47-
secure=settings.SESSION_COOKIE_SECURE or None,
48-
httponly=settings.SESSION_COOKIE_HTTPONLY or None)
31+
# First check if we need to delete this cookie.
32+
# The session should be deleted only if the session is entirely empty
33+
if settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
34+
response.delete_cookie(settings.SESSION_COOKIE_NAME,
35+
domain=settings.SESSION_COOKIE_DOMAIN)
36+
else:
37+
if accessed:
38+
patch_vary_headers(response, ('Cookie',))
39+
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
40+
if request.session.get_expire_at_browser_close():
41+
max_age = None
42+
expires = None
43+
else:
44+
max_age = request.session.get_expiry_age()
45+
expires_time = time.time() + max_age
46+
expires = cookie_date(expires_time)
47+
# Save the session data and refresh the client cookie.
48+
# Skip session save for 500 responses, refs #3881.
49+
if response.status_code != 500:
50+
request.session.save()
51+
response.set_cookie(settings.SESSION_COOKIE_NAME,
52+
request.session.session_key, max_age=max_age,
53+
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
54+
path=settings.SESSION_COOKIE_PATH,
55+
secure=settings.SESSION_COOKIE_SECURE or None,
56+
httponly=settings.SESSION_COOKIE_HTTPONLY or None)
4957
return response

django/contrib/sessions/tests.py

+70
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ def test_flush(self):
159159
self.session.flush()
160160
self.assertFalse(self.session.exists(prev_key))
161161
self.assertNotEqual(self.session.session_key, prev_key)
162+
self.assertIsNone(self.session.session_key)
162163
self.assertTrue(self.session.modified)
163164
self.assertTrue(self.session.accessed)
164165

@@ -589,6 +590,75 @@ def test_session_save_on_500(self):
589590
# Check that the value wasn't saved above.
590591
self.assertNotIn('hello', request.session.load())
591592

593+
def test_session_delete_on_end(self):
594+
request = RequestFactory().get('/')
595+
response = HttpResponse('Session test')
596+
middleware = SessionMiddleware()
597+
598+
# Before deleting, there has to be an existing cookie
599+
request.COOKIES[settings.SESSION_COOKIE_NAME] = 'abc'
600+
601+
# Simulate a request that ends the session
602+
middleware.process_request(request)
603+
request.session.flush()
604+
605+
# Handle the response through the middleware
606+
response = middleware.process_response(request, response)
607+
608+
# Check that the cookie was deleted, not recreated.
609+
# A deleted cookie header looks like:
610+
# Set-Cookie: sessionid=; expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/
611+
self.assertEqual(
612+
'Set-Cookie: {0}=; expires=Thu, 01-Jan-1970 00:00:00 GMT; '
613+
'Max-Age=0; Path=/'.format(settings.SESSION_COOKIE_NAME),
614+
str(response.cookies[settings.SESSION_COOKIE_NAME])
615+
)
616+
617+
@override_settings(SESSION_COOKIE_DOMAIN='.example.local')
618+
def test_session_delete_on_end_with_custom_domain(self):
619+
request = RequestFactory().get('/')
620+
response = HttpResponse('Session test')
621+
middleware = SessionMiddleware()
622+
623+
# Before deleting, there has to be an existing cookie
624+
request.COOKIES[settings.SESSION_COOKIE_NAME] = 'abc'
625+
626+
# Simulate a request that ends the session
627+
middleware.process_request(request)
628+
request.session.flush()
629+
630+
# Handle the response through the middleware
631+
response = middleware.process_response(request, response)
632+
633+
# Check that the cookie was deleted, not recreated.
634+
# A deleted cookie header with a custom domain looks like:
635+
# Set-Cookie: sessionid=; Domain=.example.local;
636+
# expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/
637+
self.assertEqual(
638+
'Set-Cookie: {}=; Domain=.example.local; expires=Thu, '
639+
'01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/'.format(
640+
settings.SESSION_COOKIE_NAME,
641+
),
642+
str(response.cookies[settings.SESSION_COOKIE_NAME])
643+
)
644+
645+
def test_flush_empty_without_session_cookie_doesnt_set_cookie(self):
646+
request = RequestFactory().get('/')
647+
response = HttpResponse('Session test')
648+
middleware = SessionMiddleware()
649+
650+
# Simulate a request that ends the session
651+
middleware.process_request(request)
652+
request.session.flush()
653+
654+
# Handle the response through the middleware
655+
response = middleware.process_response(request, response)
656+
657+
# A cookie should not be set.
658+
self.assertEqual(response.cookies, {})
659+
# The session is accessed so "Vary: Cookie" should be set.
660+
self.assertEqual(response['Vary'], 'Cookie')
661+
592662

593663
class CookieSessionTests(SessionTestsMixin, TestCase):
594664

docs/releases/1.4.22.txt

+18
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,21 @@ Django 1.4.22 fixes a security issue in 1.4.21.
99
It also fixes support with pip 7+ by disabling wheel support. Older versions
1010
of 1.4 would silently build a broken wheel when installed with those versions
1111
of pip.
12+
13+
Denial-of-service possibility in ``logout()`` view by filling session store
14+
===========================================================================
15+
16+
Previously, a session could be created when anonymously accessing the
17+
:func:`django.contrib.auth.views.logout` view (provided it wasn't decorated
18+
with :func:`~django.contrib.auth.decorators.login_required` as done in the
19+
admin). This could allow an attacker to easily create many new session records
20+
by sending repeated requests, potentially filling up the session store or
21+
causing other users' session records to be evicted.
22+
23+
The :class:`~django.contrib.sessions.middleware.SessionMiddleware` has been
24+
modified to no longer create empty session records.
25+
26+
Additionally, the ``contrib.sessions.backends.base.SessionBase.flush()`` and
27+
``cache_db.SessionStore.flush()`` methods have been modified to avoid creating
28+
a new empty session. Maintainers of third-party session backends should check
29+
if the same vulnerability is present in their backend and correct it if so.

docs/releases/1.7.10.txt

+18
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,21 @@ Django 1.7.10 release notes
55
*August 18, 2015*
66

77
Django 1.7.10 fixes a security issue in 1.7.9.
8+
9+
Denial-of-service possibility in ``logout()`` view by filling session store
10+
===========================================================================
11+
12+
Previously, a session could be created when anonymously accessing the
13+
:func:`django.contrib.auth.views.logout` view (provided it wasn't decorated
14+
with :func:`~django.contrib.auth.decorators.login_required` as done in the
15+
admin). This could allow an attacker to easily create many new session records
16+
by sending repeated requests, potentially filling up the session store or
17+
causing other users' session records to be evicted.
18+
19+
The :class:`~django.contrib.sessions.middleware.SessionMiddleware` has been
20+
modified to no longer create empty session records.
21+
22+
Additionally, the ``contrib.sessions.backends.base.SessionBase.flush()`` and
23+
``cache_db.SessionStore.flush()`` methods have been modified to avoid creating
24+
a new empty session. Maintainers of third-party session backends should check
25+
if the same vulnerability is present in their backend and correct it if so.

docs/topics/http/sessions.txt

+10-4
Original file line numberDiff line numberDiff line change
@@ -226,12 +226,18 @@ You can edit it multiple times.
226226

227227
.. method:: flush()
228228

229-
Delete the current session data from the session and regenerate the
230-
session key value that is sent back to the user in the cookie. This is
231-
used if you want to ensure that the previous session data can't be
232-
accessed again from the user's browser (for example, the
229+
Deletes the current session data from the session and deletes the session
230+
cookie. This is used if you want to ensure that the previous session data
231+
can't be accessed again from the user's browser (for example, the
233232
:func:`django.contrib.auth.logout()` function calls it).
234233

234+
.. versionchanged:: 1.7.10
235+
236+
Deletion of the session cookie was added. Previously, the behavior
237+
was to regenerate the session key value that was sent back to the
238+
user in the cookie, but this could be a denial-of-service
239+
vulnerability.
240+
235241
.. method:: set_test_cookie()
236242

237243
Sets a test cookie to determine whether the user's browser supports

0 commit comments

Comments
 (0)