diff --git a/src/pretix/base/middleware.py b/src/pretix/base/middleware.py
index 54a7fb89b9..43fe146474 100644
--- a/src/pretix/base/middleware.py
+++ b/src/pretix/base/middleware.py
@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# .
#
+import base64
+import hashlib
import logging
import re
from collections import OrderedDict
@@ -312,7 +314,28 @@ def _merge_csp(a, b):
for k, v in a.items():
if "'unsafe-inline'" in v:
# If we need unsafe-inline, drop any hashes or nonce as they will be ignored otherwise
- a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha-")]
+ a[k] = [i for i in v if not i.startswith("'nonce-") and not i.startswith("'sha256-")]
+
+
+def add_to_response_csp(response, csp_to_merge):
+ if "Content-Security-Policy" in response:
+ csp = _parse_csp(response["Content-Security-Policy"])
+ else:
+ csp = {}
+
+ _merge_csp(csp, csp_to_merge)
+
+ if csp:
+ response["Content-Security-Policy"] = _render_csp(csp)
+
+
+def add_to_response_csp_via_request(request, csp_to_merge):
+ _merge_csp(request._csp_to_merge, csp_to_merge)
+
+
+def calculate_csp_hash(data):
+ hash_str = base64.b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode("ascii")
+ return f"'sha256-{hash_str}'"
class SecurityMiddleware(MiddlewareMixin):
@@ -332,6 +355,9 @@ class SecurityMiddleware(MiddlewareMixin):
# but at the moment it does not seem to be an issue to just send it.
)
+ def process_request(self, request):
+ request._csp_to_merge = {}
+
def process_response(self, request, resp):
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
@@ -343,18 +369,22 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
- if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
- if 'Content-Security-Policy' in resp:
- del resp['Content-Security-Policy']
- return resp
-
- if not getattr(resp, '_csp_ignore', False):
+ if self._needs_csp(request, resp):
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp
+ def _needs_csp(self, request, resp):
+ if "Content-Type" in resp and resp["Content-Type"].split(";")[0] in self.SAFE_TYPES:
+ return False
+
+ if getattr(resp, '_csp_ignore', False):
+ return False
+
+ return True
+
def _build_csp(self, request, resp):
url = resolve(request.path_info)
@@ -387,13 +417,6 @@ class SecurityMiddleware(MiddlewareMixin):
h['style-src'] += ["'unsafe-inline'"]
h['connect-src'] += ["http://localhost:5173", "ws://localhost:5173"]
- if hasattr(request, 'csp_nonce'):
- nonce = f"'nonce-{request.csp_nonce}'"
- h['script-src'].append(nonce)
- if not settings.VITE_DEV_MODE:
- # can't have 'unsafe-inline' and nonce at the same time
- h['style-src'].append(nonce)
-
# Only include pay.google.com for wallet detection purposes on the Payment selection page
if (
url.url_name == "event.order.pay.change" or
@@ -406,6 +429,9 @@ class SecurityMiddleware(MiddlewareMixin):
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
+ if request._csp_to_merge:
+ _merge_csp(h, request._csp_to_merge)
+
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
diff --git a/src/pretix/base/templatetags/vite.py b/src/pretix/base/templatetags/vite.py
index 3c0303c9b3..3473889890 100644
--- a/src/pretix/base/templatetags/vite.py
+++ b/src/pretix/base/templatetags/vite.py
@@ -19,12 +19,10 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# .
#
-
import json
import logging
import pathlib
import re
-import secrets
from urllib.parse import urljoin
from urllib.request import urlopen
@@ -33,6 +31,10 @@ from django import template
from django.conf import settings
from django.utils.safestring import mark_safe
+from pretix.base.middleware import (
+ add_to_response_csp_via_request, calculate_csp_hash,
+)
+
register = template.Library()
LOGGER = logging.getLogger(__name__)
_MANIFEST = {}
@@ -234,10 +236,16 @@ def vite_importmap(context):
if not imports:
return ""
- # Generate a nonce and store it on the request so the CSP middleware can allow it
- nonce = secrets.token_urlsafe(16)
+ json_string = json.dumps({"imports": imports})
+
+ # Calculate hash and add it to the CSP info in the request, so that will be merged into the
+ # response header later on by the middleware
request = context.get('request')
if request:
- request.csp_nonce = nonce
+ csp_hash = calculate_csp_hash(json_string)
+ add_to_response_csp_via_request(request, {
+ 'style-src': [csp_hash],
+ 'script-src': [csp_hash],
+ })
- return f''
+ return f''
diff --git a/src/pretix/control/views/mail.py b/src/pretix/control/views/mail.py
index d6e52cface..21dbd8c4bf 100644
--- a/src/pretix/control/views/mail.py
+++ b/src/pretix/control/views/mail.py
@@ -35,7 +35,7 @@ from django.utils.translation import ngettext
from django.views import View
from django.views.generic import DetailView, ListView
-from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
+from pretix.base.middleware import add_to_response_csp
from pretix.base.models import OutgoingMail
from pretix.base.services.mail import mail_send_task
from pretix.control.forms.filter import OutgoingMailFilterForm
@@ -108,18 +108,12 @@ class OutgoingMailDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequir
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
- if 'Content-Security-Policy' in response:
- h = _parse_csp(response['Content-Security-Policy'])
- else:
- h = {}
- csps = {
+ add_to_response_csp(response, {
'frame-src': ['data:'],
# Unfortuantely, we can't avoid unsafe-inline for style here.
# See outgoingmail.js for the protection measures we take.
'style-src': ["'unsafe-inline'"],
- }
- _merge_csp(h, csps)
- response['Content-Security-Policy'] = _render_csp(h)
+ })
return response
def get_context_data(self, **kwargs):
diff --git a/src/pretix/plugins/paypal2/signals.py b/src/pretix/plugins/paypal2/signals.py
index 7965f8e0b0..00932625ce 100644
--- a/src/pretix/plugins/paypal2/signals.py
+++ b/src/pretix/plugins/paypal2/signals.py
@@ -32,7 +32,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.forms import SecretKeySettingsField
from pretix.base.logentrytypes import EventLogEntryType, log_entry_types
-from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
+from pretix.base.middleware import add_to_response_csp
from pretix.base.settings import settings_hierarkey
from pretix.base.signals import (
register_global_settings, register_payment_providers,
@@ -144,12 +144,7 @@ def signal_process_response(sender, request: HttpRequest, response: HttpResponse
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
(url.namespace == "plugins:paypal2" and url.url_name == "pay")
):
- if 'Content-Security-Policy' in response:
- h = _parse_csp(response['Content-Security-Policy'])
- else:
- h = {}
-
- csps = {
+ add_to_response_csp(response, {
'script-src': ['https://www.paypal.com', "'nonce-{}'".format(_nonce(request))],
# When the stars align in an unpredictable manner and the temperature is just right, the PayPal SDK might
@@ -164,12 +159,7 @@ def signal_process_response(sender, request: HttpRequest, response: HttpResponse
'connect-src': ['https://www.paypal.com', 'https://www.sandbox.paypal.com'], # Or not - seems to only affect PayPal logging...
'img-src': ['https://t.paypal.com', 'https://www.paypalobjects.com'],
'style-src': ["'unsafe-inline'"] # PayPal does not comply with our nonce unfortunately, see Z#23113213
- }
-
- _merge_csp(h, csps)
-
- if h:
- response['Content-Security-Policy'] = _render_csp(h)
+ })
return response
diff --git a/src/pretix/plugins/stripe/signals.py b/src/pretix/plugins/stripe/signals.py
index 0c7b328d09..460d7623d4 100644
--- a/src/pretix/plugins/stripe/signals.py
+++ b/src/pretix/plugins/stripe/signals.py
@@ -31,7 +31,7 @@ from django.utils.translation import gettext_lazy as _
from paypalhttp import HttpResponse
from pretix.base.forms import SecretKeySettingsField
-from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
+from pretix.base.middleware import add_to_response_csp
from pretix.base.settings import settings_hierarkey
from pretix.base.signals import (
logentry_display, register_global_settings, register_payment_providers,
@@ -201,21 +201,10 @@ def signal_process_response(sender, request: HttpRequest, response: HttpResponse
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
(url.namespace == "plugins:stripe" and url.url_name in ["sca", "sca.return"])
):
- if 'Content-Security-Policy' in response:
- h = _parse_csp(response['Content-Security-Policy'])
- else:
- h = {}
-
- # https://stripe.com/docs/security/guide#content-security-policy
- csps = {
+ add_to_response_csp(response, {
'connect-src': ['https://api.stripe.com'],
'frame-src': ['https://js.stripe.com', 'https://hooks.stripe.com'],
'script-src': ['https://js.stripe.com'],
- }
-
- _merge_csp(h, csps)
-
- if h:
- response['Content-Security-Policy'] = _render_csp(h)
+ })
return response