mirror of
https://github.com/pretix/pretix.git
synced 2026-05-26 18:43:59 +00:00
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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user