forked from CGM_Public/pretix_original
Fix delete_cookie for partitioned legacy CSRF cookie
This commit is contained in:
committed by
pajowu
parent
779ab360df
commit
e19908571c
@@ -19,8 +19,10 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import itertools
|
||||
import re
|
||||
|
||||
from http.cookies import Morsel
|
||||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@@ -48,6 +50,41 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
|
||||
response.cookies[key]['Partitioned'] = True
|
||||
|
||||
|
||||
def thoroughly_delete_cookie(response, cookie_name, **kwargs):
|
||||
""" Deletes different possible versions of a cookie (SameSite, Partitioned) """
|
||||
properties = {"SameSite": ["", 'None'], "Partitioned": ["", True]}
|
||||
for i, values in enumerate(itertools.product(*properties.values())):
|
||||
m = Morsel()
|
||||
m.set(cookie_name, '', '')
|
||||
m.update(kwargs)
|
||||
m.update(zip(properties.keys(), values))
|
||||
m['expires'] = "Thu, 01 Jan 1970 00:00:00 GMT"
|
||||
|
||||
response.cookies[f'___DELETECOOKIE__{i}___{cookie_name}'] = m
|
||||
|
||||
# Make sure settings a cookie afterwards will add a new item in the dictionary, placing
|
||||
# it below our deletion headers.
|
||||
response.cookies.pop(cookie_name, None)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
# Based on https://www.chromium.org/updates/same-site/incompatible-clients
|
||||
# Copyright 2019 Google LLC.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -34,8 +34,6 @@
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from http.cookies import Morsel
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
@@ -58,7 +56,10 @@ from django.utils.http import http_date
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import Event, Organizer
|
||||
from pretix.helpers.cookies import set_cookie_without_samesite
|
||||
from pretix.helpers.cookies import (
|
||||
get_all_values_of_cookie, set_cookie_without_samesite,
|
||||
thoroughly_delete_cookie,
|
||||
)
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -181,9 +182,23 @@ class SessionMiddleware(BaseSessionMiddleware):
|
||||
# The session should be deleted only if the session is entirely empty
|
||||
is_secure = request.scheme == 'https'
|
||||
if '__Host-' + settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
|
||||
response.delete_cookie('__Host-' + settings.SESSION_COOKIE_NAME)
|
||||
# response.delete_cookie does not work as we might have set a partitioned cookie
|
||||
thoroughly_delete_cookie(
|
||||
response,
|
||||
'__Host-' + settings.SESSION_COOKIE_NAME,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=is_secure,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None
|
||||
)
|
||||
elif settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
|
||||
response.delete_cookie(settings.SESSION_COOKIE_NAME)
|
||||
# response.delete_cookie does not work as we might have set a partitioned cookie
|
||||
thoroughly_delete_cookie(
|
||||
response,
|
||||
settings.SESSION_COOKIE_NAME,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=is_secure,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None
|
||||
)
|
||||
else:
|
||||
if accessed:
|
||||
patch_vary_headers(response, ('Cookie',))
|
||||
@@ -200,15 +215,20 @@ class SessionMiddleware(BaseSessionMiddleware):
|
||||
if response.status_code != 500:
|
||||
request.session.save()
|
||||
if is_secure and settings.SESSION_COOKIE_NAME in request.COOKIES: # remove legacy cookie
|
||||
response.delete_cookie(settings.SESSION_COOKIE_NAME)
|
||||
response.delete_cookie(settings.SESSION_COOKIE_NAME, samesite="None")
|
||||
# response.delete_cookie does not work as we might have set a partitioned cookie
|
||||
thoroughly_delete_cookie(response,
|
||||
settings.SESSION_COOKIE_NAME,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=is_secure,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None
|
||||
)
|
||||
set_cookie_without_samesite(
|
||||
request, response,
|
||||
'__Host-' + settings.SESSION_COOKIE_NAME if is_secure else settings.SESSION_COOKIE_NAME,
|
||||
request.session.session_key, max_age=max_age,
|
||||
expires=expires,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=request.scheme == 'https',
|
||||
secure=is_secure,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None
|
||||
)
|
||||
return response
|
||||
@@ -253,22 +273,27 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
|
||||
if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
|
||||
request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
|
||||
else:
|
||||
is_secure = request.scheme == 'https'
|
||||
# Set the CSRF cookie even if it's already set, so we renew
|
||||
# the expiry timer.
|
||||
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, samesite="None")
|
||||
|
||||
# remove legacy cookie
|
||||
if request.is_secure() and settings.CSRF_COOKIE_NAME in request.COOKIES:
|
||||
thoroughly_delete_cookie(response,
|
||||
settings.CSRF_COOKIE_NAME,
|
||||
path=settings.CSRF_COOKIE_PATH,
|
||||
secure=request.is_secure(),
|
||||
httponly=settings.CSRF_COOKIE_HTTPONLY
|
||||
)
|
||||
|
||||
handle_duplicated_csrftoken(request, response)
|
||||
|
||||
set_cookie_without_samesite(
|
||||
request, response,
|
||||
'__Host-' + settings.CSRF_COOKIE_NAME if is_secure else settings.CSRF_COOKIE_NAME,
|
||||
'__Host-' + settings.CSRF_COOKIE_NAME if request.is_secure() else settings.CSRF_COOKIE_NAME,
|
||||
request.META["CSRF_COOKIE"],
|
||||
max_age=settings.CSRF_COOKIE_AGE,
|
||||
path=settings.CSRF_COOKIE_PATH,
|
||||
secure=is_secure,
|
||||
secure=request.is_secure(),
|
||||
httponly=settings.CSRF_COOKIE_HTTPONLY
|
||||
)
|
||||
# Content varies with the CSRF cookie, so set the Vary header.
|
||||
@@ -276,52 +301,20 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
|
||||
|
||||
|
||||
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
|
||||
# Due to a Safari bug, in some browser, two csrftoken cookies can exist:
|
||||
# one unpartitioned, one partitioned. This function generates an additional
|
||||
# Set-Cookie header to get rid of the unpartitioned one.
|
||||
|
||||
cookie_name = '__Host-' + settings.CSRF_COOKIE_NAME
|
||||
|
||||
if request.scheme == 'https' and cookie_name in request.COOKIES:
|
||||
if request.is_secure() and cookie_name in request.COOKIES:
|
||||
values = get_all_values_of_cookie(request.headers.get('Cookie'), cookie_name)
|
||||
if len(values) > 1:
|
||||
logger.info('Trying to remove duplicated %s cookies: %r', cookie_name, values)
|
||||
|
||||
# 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(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___' + cookie_name] = make_delete_morsel(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 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
|
||||
thoroughly_delete_cookie(
|
||||
response,
|
||||
cookie_name,
|
||||
secure=request.is_secure(),
|
||||
path=settings.CSRF_COOKIE_PATH,
|
||||
httponly=settings.CSRF_COOKIE_HTTPONLY
|
||||
)
|
||||
Reference in New Issue
Block a user