Add __Host- prefix to CSRF and session cookie, remove cookie_domain (#3831)

* Add __Host- prefix to CSRF and session cookie, remove cookie_domain

* Fix tests
This commit is contained in:
Raphael Michel
2024-01-25 09:45:56 +01:00
committed by GitHub
parent dba8e80868
commit 6af2d38a98
6 changed files with 59 additions and 42 deletions

View File

@@ -40,10 +40,12 @@ from django.contrib.sessions.middleware import (
SessionMiddleware as BaseSessionMiddleware,
)
from django.core.cache import cache
from django.core.exceptions import DisallowedHost
from django.core.exceptions import DisallowedHost, ImproperlyConfigured
from django.http.request import split_domain_port
from django.middleware.csrf import (
CSRF_SESSION_KEY, CsrfViewMiddleware as BaseCsrfMiddleware,
CSRF_SESSION_KEY, CSRF_TOKEN_LENGTH,
CsrfViewMiddleware as BaseCsrfMiddleware, _check_token_format,
_unmask_cipher_token,
)
from django.shortcuts import render
from django.urls import set_urlconf
@@ -144,6 +146,13 @@ class SessionMiddleware(BaseSessionMiddleware):
a custom domain.
"""
def process_request(self, request):
session_key = request.COOKIES.get(
'__Host-' + settings.SESSION_COOKIE_NAME,
request.COOKIES.get(settings.SESSION_COOKIE_NAME)
)
request.session = self.SessionStore(session_key)
def process_response(self, request, response):
try:
accessed = request.session.accessed
@@ -154,7 +163,10 @@ class SessionMiddleware(BaseSessionMiddleware):
else:
# First check if we need to delete this cookie.
# The session should be deleted only if the session is entirely empty
if settings.SESSION_COOKIE_NAME in request.COOKIES and 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)
elif settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
response.delete_cookie(settings.SESSION_COOKIE_NAME)
else:
if accessed:
@@ -171,12 +183,14 @@ class SessionMiddleware(BaseSessionMiddleware):
# Skip session save for 500 responses, refs #3881.
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")
set_cookie_without_samesite(
request, response,
settings.SESSION_COOKIE_NAME,
'__Host-' + settings.SESSION_COOKIE_NAME if is_secure else settings.SESSION_COOKIE_NAME,
request.session.session_key, max_age=max_age,
expires=expires,
domain=get_cookie_domain(request),
path=settings.SESSION_COOKIE_PATH,
secure=request.scheme == 'https',
httponly=settings.SESSION_COOKIE_HTTPONLY or None
@@ -191,38 +205,52 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
a custom domain.
"""
def _get_secret(self, request):
if settings.CSRF_USE_SESSIONS:
try:
csrf_secret = request.session.get(CSRF_SESSION_KEY)
except AttributeError:
raise ImproperlyConfigured(
"CSRF_USE_SESSIONS is enabled, but request.session is not "
"set. SessionMiddleware must appear before CsrfViewMiddleware "
"in MIDDLEWARE."
)
else:
try:
csrf_secret = request.COOKIES.get('__Host-' + settings.CSRF_COOKIE_NAME)
if not csrf_secret:
csrf_secret = request.COOKIES[settings.CSRF_COOKIE_NAME]
except KeyError:
csrf_secret = None
else:
# This can raise InvalidTokenFormat.
_check_token_format(csrf_secret)
if csrf_secret is None:
return None
# Django versions before 4.0 masked the secret before storing.
if len(csrf_secret) == CSRF_TOKEN_LENGTH:
csrf_secret = _unmask_cipher_token(csrf_secret)
return csrf_secret
def _set_csrf_cookie(self, request, response):
if settings.CSRF_USE_SESSIONS:
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")
set_cookie_without_samesite(
request, response,
settings.CSRF_COOKIE_NAME,
'__Host-' + settings.CSRF_COOKIE_NAME if is_secure else settings.CSRF_COOKIE_NAME,
request.META["CSRF_COOKIE"],
max_age=settings.CSRF_COOKIE_AGE,
domain=get_cookie_domain(request),
path=settings.CSRF_COOKIE_PATH,
secure=request.scheme == 'https',
secure=is_secure,
httponly=settings.CSRF_COOKIE_HTTPONLY
)
# Content varies with the CSRF cookie, so set the Vary header.
patch_vary_headers(response, ('Cookie',))
def get_cookie_domain(request):
if '.' not in request.host:
# As per spec, browsers do not accept cookie domains without dots in it,
# e.g. "localhost", see http://curl.haxx.se/rfc/cookie_spec.html
return None
default_domain, default_port = split_domain_port(urlparse(settings.SITE_URL).netloc)
if request.host == default_domain:
# We are on our main domain, set the cookie domain the user has chosen
return settings.SESSION_COOKIE_DOMAIN
else:
# We are on an organizer's custom domain, set no cookie domain, as we do not want
# the cookies to be present on any other domain. Setting an explicit value can be
# dangerous, see http://erik.io/blog/2014/03/04/definitive-guide-to-cookie-domains/
return None

View File

@@ -54,7 +54,6 @@ from pretix.base.models import (
from pretix.base.services.cart import get_fees
from pretix.base.templatetags.money import money_filter
from pretix.helpers.cookies import set_cookie_without_samesite
from pretix.multidomain.middlewares import get_cookie_domain
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.signals import question_form_fields
@@ -469,7 +468,6 @@ def iframe_entry_view_wrapper(view_func):
locale,
max_age=max_age,
expires=(datetime.utcnow() + timedelta(seconds=max_age)).strftime('%a, %d-%b-%Y %H:%M:%S GMT'),
domain=get_cookie_domain(request)
)
return resp

View File

@@ -40,7 +40,6 @@ from django.utils.http import url_has_allowed_host_and_scheme
from django.views.generic import View
from pretix.helpers.cookies import set_cookie_without_samesite
from pretix.multidomain.middlewares import get_cookie_domain
from .robots import NoSearchIndexViewMixin
@@ -63,7 +62,6 @@ class LocaleSet(NoSearchIndexViewMixin, View):
max_age=max_age,
expires=(datetime.utcnow() + timedelta(seconds=max_age)).strftime(
'%a, %d-%b-%Y %H:%M:%S GMT'),
domain=get_cookie_domain(request)
)
return resp

View File

@@ -335,8 +335,6 @@ if HAS_CELERY:
else:
CELERY_TASK_ALWAYS_EAGER = True
SESSION_COOKIE_DOMAIN = config.get('pretix', 'cookie_domain', fallback=None)
CACHE_TICKETS_HOURS = config.getint('cache', 'tickets', fallback=24 * 3)
ENTROPY = {