Remove duplicated csrftoken cookies

Due to a Safari bug, in some browser, two csrftoken cookies with different values
exist: one unpartitioned, one partitioned ("CHIPS"). This function generates an
additional Set-Cookie header to get rid of the unpartitioned one.

As Django usually only allows one Set-Cookie header per cookie name, we
need to manually create a cookie 'Morsel' for the deletion and store it
in the HttpResponse's cookie dictionary under a different name, so it is
not overwritten by the actual, correct Set-Cookie header. This works
because the code in django.core.handlers.wsgi/asgi, that generates the
actual Set-Cookie headers, only iterates over cookie.values(), ignoring
the keys.
This commit is contained in:
Mira Weller
2026-05-22 16:07:10 +02:00
parent 18485f5d95
commit 7554193e2a

View File

@@ -33,6 +33,8 @@
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
import time import time
from datetime import datetime
from http.cookies import Morsel
from urllib.parse import urlparse from urllib.parse import urlparse
from django.conf import settings from django.conf import settings
@@ -254,6 +256,9 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
if is_secure and settings.CSRF_COOKIE_NAME in request.COOKIES: # remove legacy cookie if is_secure and settings.CSRF_COOKIE_NAME in request.COOKIES: # remove legacy cookie
response.delete_cookie(settings.CSRF_COOKIE_NAME) response.delete_cookie(settings.CSRF_COOKIE_NAME)
response.delete_cookie(settings.CSRF_COOKIE_NAME, samesite="None") response.delete_cookie(settings.CSRF_COOKIE_NAME, samesite="None")
handle_duplicated_csrftoken(request, response)
set_cookie_without_samesite( set_cookie_without_samesite(
request, response, request, response,
'__Host-' + settings.CSRF_COOKIE_NAME if is_secure else settings.CSRF_COOKIE_NAME, '__Host-' + settings.CSRF_COOKIE_NAME if is_secure else settings.CSRF_COOKIE_NAME,
@@ -265,3 +270,54 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
) )
# Content varies with the CSRF cookie, so set the Vary header. # Content varies with the CSRF cookie, so set the Vary header.
patch_vary_headers(response, ('Cookie',)) patch_vary_headers(response, ('Cookie',))
def handle_duplicated_csrftoken(request, response):
# Due to a Safari bug, in some browser, two csrftoken cookies with different values
# exist: one unpartitioned, one partitioned. This function generates an additional
# Set-Cookie header to get rid of the unpartitioned one.
if request.scheme == 'https' and '__Host-' + settings.CSRF_COOKIE_NAME and has_duplicated_csrftoken(request):
# Make sure the set_cookie_without_samesite below will add a new item in the dictionary, placing
# it below our deletion header.
response.cookies.pop('__Host-' + settings.CSRF_COOKIE_NAME, None)
# Add the deletion Set-Cookie header to the cookie dict under a wrong name, so it doesn't get
# overwritten by the set_cookie_without_samesite call below. This works because the code in
# django.core.handlers.wsgi/asgi, that generates the actual Set-Cookie headers, only iterates
# over cookie.values(), ignoring the keys.
response.cookies['___DELETECOOKIE___' + '__Host-' + settings.CSRF_COOKIE_NAME] = make_delete_morsel('__Host-' + settings.CSRF_COOKIE_NAME)
def get_all_values_of_cookie(cookie_header, cookie_name):
# like django.http.cookie.parse_cookie, but returns all values of duplicated cookies instead of only the last
values = list()
if not cookie_header:
return values
for chunk in cookie_header.split(";"):
if "=" in chunk:
key, val = chunk.split("=", 1)
else:
# Assume an empty name per
# https://bugzilla.mozilla.org/show_bug.cgi?id=169091
key, val = "", chunk
key, val = key.strip(), val.strip()
if key == cookie_name:
values.append(val)
return values
def has_duplicated_csrftoken(request):
values = get_all_values_of_cookie(request.headers.get('Cookie'), '__Host-' + settings.CSRF_COOKIE_NAME)
return len(values) > 1
def make_delete_morsel(name):
m = Morsel()
m.set(name, '', '')
m['expires'] = datetime.utcfromtimestamp(0).strftime("%a, %d %b %Y %H:%M:%S GMT")
m['samesite'] = 'None'
m['secure'] = True
m['path'] = settings.CSRF_COOKIE_PATH
m['httponly'] = settings.CSRF_COOKIE_HTTPONLY
return m