forked from CGM_Public/pretix_original
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:
@@ -42,7 +42,6 @@ Example::
|
|||||||
currency=EUR
|
currency=EUR
|
||||||
datadir=/data
|
datadir=/data
|
||||||
plugins_default=pretix.plugins.sendmail,pretix.plugins.statistics
|
plugins_default=pretix.plugins.sendmail,pretix.plugins.statistics
|
||||||
cookie_domain=.pretix.de
|
|
||||||
|
|
||||||
``instance_name``
|
``instance_name``
|
||||||
The name of this installation. Default: ``pretix.de``
|
The name of this installation. Default: ``pretix.de``
|
||||||
@@ -71,9 +70,6 @@ Example::
|
|||||||
``auth_backends``
|
``auth_backends``
|
||||||
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
|
A comma-separated list of available auth backends. Defaults to ``pretix.base.auth.NativeAuthBackend``.
|
||||||
|
|
||||||
``cookie_domain``
|
|
||||||
The cookie domain to be set. Defaults to ``None``.
|
|
||||||
|
|
||||||
``registration``
|
``registration``
|
||||||
Enables or disables the registration of new admin users. Defaults to ``off``.
|
Enables or disables the registration of new admin users. Defaults to ``off``.
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,12 @@ from django.contrib.sessions.middleware import (
|
|||||||
SessionMiddleware as BaseSessionMiddleware,
|
SessionMiddleware as BaseSessionMiddleware,
|
||||||
)
|
)
|
||||||
from django.core.cache import cache
|
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.http.request import split_domain_port
|
||||||
from django.middleware.csrf import (
|
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.shortcuts import render
|
||||||
from django.urls import set_urlconf
|
from django.urls import set_urlconf
|
||||||
@@ -144,6 +146,13 @@ class SessionMiddleware(BaseSessionMiddleware):
|
|||||||
a custom domain.
|
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):
|
def process_response(self, request, response):
|
||||||
try:
|
try:
|
||||||
accessed = request.session.accessed
|
accessed = request.session.accessed
|
||||||
@@ -154,7 +163,10 @@ class SessionMiddleware(BaseSessionMiddleware):
|
|||||||
else:
|
else:
|
||||||
# First check if we need to delete this cookie.
|
# First check if we need to delete this cookie.
|
||||||
# The session should be deleted only if the session is entirely empty
|
# 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)
|
response.delete_cookie(settings.SESSION_COOKIE_NAME)
|
||||||
else:
|
else:
|
||||||
if accessed:
|
if accessed:
|
||||||
@@ -171,12 +183,14 @@ class SessionMiddleware(BaseSessionMiddleware):
|
|||||||
# Skip session save for 500 responses, refs #3881.
|
# Skip session save for 500 responses, refs #3881.
|
||||||
if response.status_code != 500:
|
if response.status_code != 500:
|
||||||
request.session.save()
|
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(
|
set_cookie_without_samesite(
|
||||||
request, response,
|
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,
|
request.session.session_key, max_age=max_age,
|
||||||
expires=expires,
|
expires=expires,
|
||||||
domain=get_cookie_domain(request),
|
|
||||||
path=settings.SESSION_COOKIE_PATH,
|
path=settings.SESSION_COOKIE_PATH,
|
||||||
secure=request.scheme == 'https',
|
secure=request.scheme == 'https',
|
||||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None
|
httponly=settings.SESSION_COOKIE_HTTPONLY or None
|
||||||
@@ -191,38 +205,52 @@ class CsrfViewMiddleware(BaseCsrfMiddleware):
|
|||||||
a custom domain.
|
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):
|
def _set_csrf_cookie(self, request, response):
|
||||||
if settings.CSRF_USE_SESSIONS:
|
if settings.CSRF_USE_SESSIONS:
|
||||||
if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
|
if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
|
||||||
request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
|
request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
|
||||||
else:
|
else:
|
||||||
|
is_secure = request.scheme == 'https'
|
||||||
# Set the CSRF cookie even if it's already set, so we renew
|
# Set the CSRF cookie even if it's already set, so we renew
|
||||||
# the expiry timer.
|
# 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(
|
set_cookie_without_samesite(
|
||||||
request, response,
|
request, response,
|
||||||
settings.CSRF_COOKIE_NAME,
|
'__Host-' + settings.CSRF_COOKIE_NAME if is_secure else settings.CSRF_COOKIE_NAME,
|
||||||
request.META["CSRF_COOKIE"],
|
request.META["CSRF_COOKIE"],
|
||||||
max_age=settings.CSRF_COOKIE_AGE,
|
max_age=settings.CSRF_COOKIE_AGE,
|
||||||
domain=get_cookie_domain(request),
|
|
||||||
path=settings.CSRF_COOKIE_PATH,
|
path=settings.CSRF_COOKIE_PATH,
|
||||||
secure=request.scheme == 'https',
|
secure=is_secure,
|
||||||
httponly=settings.CSRF_COOKIE_HTTPONLY
|
httponly=settings.CSRF_COOKIE_HTTPONLY
|
||||||
)
|
)
|
||||||
# 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 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
|
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ from pretix.base.models import (
|
|||||||
from pretix.base.services.cart import get_fees
|
from pretix.base.services.cart import get_fees
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.helpers.cookies import set_cookie_without_samesite
|
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.multidomain.urlreverse import eventreverse
|
||||||
from pretix.presale.signals import question_form_fields
|
from pretix.presale.signals import question_form_fields
|
||||||
|
|
||||||
@@ -469,7 +468,6 @@ def iframe_entry_view_wrapper(view_func):
|
|||||||
locale,
|
locale,
|
||||||
max_age=max_age,
|
max_age=max_age,
|
||||||
expires=(datetime.utcnow() + timedelta(seconds=max_age)).strftime('%a, %d-%b-%Y %H:%M:%S GMT'),
|
expires=(datetime.utcnow() + timedelta(seconds=max_age)).strftime('%a, %d-%b-%Y %H:%M:%S GMT'),
|
||||||
domain=get_cookie_domain(request)
|
|
||||||
)
|
)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ from django.utils.http import url_has_allowed_host_and_scheme
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from pretix.helpers.cookies import set_cookie_without_samesite
|
from pretix.helpers.cookies import set_cookie_without_samesite
|
||||||
from pretix.multidomain.middlewares import get_cookie_domain
|
|
||||||
|
|
||||||
from .robots import NoSearchIndexViewMixin
|
from .robots import NoSearchIndexViewMixin
|
||||||
|
|
||||||
@@ -63,7 +62,6 @@ class LocaleSet(NoSearchIndexViewMixin, View):
|
|||||||
max_age=max_age,
|
max_age=max_age,
|
||||||
expires=(datetime.utcnow() + timedelta(seconds=max_age)).strftime(
|
expires=(datetime.utcnow() + timedelta(seconds=max_age)).strftime(
|
||||||
'%a, %d-%b-%Y %H:%M:%S GMT'),
|
'%a, %d-%b-%Y %H:%M:%S GMT'),
|
||||||
domain=get_cookie_domain(request)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|||||||
@@ -335,8 +335,6 @@ if HAS_CELERY:
|
|||||||
else:
|
else:
|
||||||
CELERY_TASK_ALWAYS_EAGER = True
|
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)
|
CACHE_TICKETS_HOURS = config.getint('cache', 'tickets', fallback=24 * 3)
|
||||||
|
|
||||||
ENTROPY = {
|
ENTROPY = {
|
||||||
|
|||||||
@@ -165,11 +165,10 @@ def test_cookie_domain_on_event_domain(env, client):
|
|||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_cookie_domain_on_main_domain(env, client):
|
def test_cookie_domain_on_main_domain(env, client):
|
||||||
with override_settings(SESSION_COOKIE_DOMAIN='example.com'):
|
client.post('/mrmcd/2015/cart/add', HTTP_HOST='example.com')
|
||||||
client.post('/mrmcd/2015/cart/add', HTTP_HOST='example.com')
|
r = client.get('/mrmcd/2015/', HTTP_HOST='example.com')
|
||||||
r = client.get('/mrmcd/2015/', HTTP_HOST='example.com')
|
assert r.client.cookies['pretix_csrftoken']['domain'] == ''
|
||||||
assert r.client.cookies['pretix_csrftoken']['domain'] == 'example.com'
|
assert r.client.cookies['pretix_session']['domain'] == ''
|
||||||
assert r.client.cookies['pretix_session']['domain'] == 'example.com'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -200,8 +199,8 @@ def test_cookie_samesite_none(env, client, agent):
|
|||||||
client.post('/mrmcd/2015/cart/add', HTTP_HOST='example.com', HTTP_USER_AGENT=agent,
|
client.post('/mrmcd/2015/cart/add', HTTP_HOST='example.com', HTTP_USER_AGENT=agent,
|
||||||
secure=True)
|
secure=True)
|
||||||
r = client.get('/mrmcd/2015/', HTTP_HOST='example.com', HTTP_USER_AGENT=agent, secure=True)
|
r = client.get('/mrmcd/2015/', HTTP_HOST='example.com', HTTP_USER_AGENT=agent, secure=True)
|
||||||
assert r.client.cookies['pretix_csrftoken']['samesite'] == 'None'
|
assert r.client.cookies['__Host-pretix_csrftoken']['samesite'] == 'None'
|
||||||
assert r.client.cookies['pretix_session']['samesite'] == 'None'
|
assert r.client.cookies['__Host-pretix_session']['samesite'] == 'None'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -220,4 +219,4 @@ def test_cookie_samesite_none(env, client, agent):
|
|||||||
def test_cookie_samesite_none_only_on_compatible_browsers(env, client, agent):
|
def test_cookie_samesite_none_only_on_compatible_browsers(env, client, agent):
|
||||||
client.post('/mrmcd/2015/cart/add', HTTP_HOST='example.com', HTTP_USER_AGENT=agent, secure=True)
|
client.post('/mrmcd/2015/cart/add', HTTP_HOST='example.com', HTTP_USER_AGENT=agent, secure=True)
|
||||||
r = client.get('/mrmcd/2015/', HTTP_HOST='example.com', HTTP_USER_AGENT=agent, secure=True)
|
r = client.get('/mrmcd/2015/', HTTP_HOST='example.com', HTTP_USER_AGENT=agent, secure=True)
|
||||||
assert not r.client.cookies['pretix_csrftoken'].get('samesite')
|
assert not r.client.cookies['__Host-pretix_csrftoken'].get('samesite')
|
||||||
|
|||||||
Reference in New Issue
Block a user