Opened 3 weeks ago

Last modified 2 days ago

#37249 assigned Bug

Race condition can undo logout with cache/file session backends

Reported by: Shai Berger Owned by: Vishy
Component: contrib.sessions Version: dev
Severity: Normal Keywords: not-security
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Django’s cache and file session backends can resurrect a session that was deleted by logout under a narrow but concrete concurrent-request race.

The vulnerable interleaving occurs when an authenticated same-session request has already reached SessionMiddleware.process_response() and the backend save() path has checked that the old session still exists. If /logout/ deletes that same session before the backend performs its final write, the cache/file backend can write the old session key again. SessionMiddleware then raises no SessionInterrupted and sends Set-Cookie for the old sessionid.

This can restore authenticated session data after logout if the late response is applied as the browser’s final cookie state.

The "signed_cookie", "db" and "cached_db" session backends are not affected.

Root Cause

The DB session backend updates an existing row using force_update=True. If logout deletes the row before a late save, the update fails and Django
raises UpdateError, which SessionMiddleware converts to SessionInterrupted.

The cache and file backends use a check-then-write pattern instead:

  • Cache backend checks existence with cache.get() and then writes with cache.set().
  • File backend checks that the target file exists, then writes a temporary file and moves it into the target path.

Those final writes are not atomically tied to the existence check. If logout deletes the same session after the check but before the final write,
the old key/path is recreated instead of failing.

Failure Scenario

A user has an authenticated session, and two same-session requests are in flight concurrently. This can occur through multiple tabs, background requests, delayed form submissions, beacons/fetches, or application endpoints that modify session state around the same time as logout.

The problematic interleaving is:

  1. Request A modifies the session and reaches SessionMiddleware.process_response().
  2. Request A’s backend save() path confirms the old session still exists.
  3. Request B calls /logout/ and deletes the same session.
  4. Request A performs its final backend write and recreates the old session key.
  5. Request A’s late response sends Set-Cookie: sessionid=<old key>.
  6. If the browser applies Request A’s response after logout, a later /whoami/ request is authenticated again.

This requires a concurrent same-session request and the narrow save-window interleaving.

Important Non-Claim

The simpler flow below was tested and is not sufficient:

  1. /slow-save/ pauses inside the view.
  2. /logout/ deletes the session.
  3. /slow-save/ later finalizes.

In that ordering, cache/file/db/cached_db correctly raise SessionInterrupted
or otherwise do not restore authentication.

Credit

This, including the attached test script, was reported to the Security Team by Jaeyoung Jang (@BORAMAE). In the ticket, some edits were made to the original report.

Attachments (1)

session_resurrection_race_repro.py (9.4 KB ) - added by Shai Berger 3 weeks ago.

Download all attachments as: .zip

Change History (6)

by Shai Berger, 3 weeks ago

comment:1 by Shai Berger, 3 weeks ago

The attached reproduction script builds a minimal Django app with real auth/session middleware and three endpoints:

  • /slow-save/: requires authentication, reads and modifies request.session.
  • /logout/: calls django.contrib.auth.logout(request).
  • /whoami/: returns authentication state and current session key.

For the positive cache/file tests, the script deterministically pauses the backend between the existence check and final write, calls /logout/, then releases the late save.

Run command:

cd /path/to/file
python session_resurrection_race_repro.py

Expected result:

logout_deleted_cookie: true
exists_after_logout: false
exists_after_slow: true
slow_set_cookie_same_old_key: true
/whoami/ returns authenticated: true

comment:2 by Natalia Bidart, 3 weeks ago

Keywords: not-security added
Triage Stage: UnreviewedAccepted

comment:3 by Jacob Walls, 3 weeks ago

Solving this in the file backend is probably doable with locks. I read the discussion in the patches for #8616, and there was a lot of discussion about avoiding long-held-open locks, so we would need to check this carefully.

Solving this in the cache backends would require new cache API IMO like replace() to take advantage of locking in redis or memcached.

There is also the asave() method on the cache interface to think about for races.

comment:4 by Vishy, 3 weeks ago

Owner: set to Vishy
Status: newassigned

comment:5 by Vishy, 2 days ago

Owner: changed from Vishy to Vishy
Note: See TracTickets for help on using tickets.
Back to Top