Improve CSP handling in vite integration; refactor CSP handling into helper functions (Z#23240534) (#6387)

This commit is contained in:
luelista
2026-07-21 13:20:31 +02:00
committed by GitHub
parent 930e153db4
commit cc082e2001
5 changed files with 63 additions and 56 deletions
+40 -14
View File
@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
import base64
import hashlib
import logging import logging
import re import re
from collections import OrderedDict from collections import OrderedDict
@@ -312,7 +314,28 @@ def _merge_csp(a, b):
for k, v in a.items(): for k, v in a.items():
if "'unsafe-inline'" in v: if "'unsafe-inline'" in v:
# If we need unsafe-inline, drop any hashes or nonce as they will be ignored otherwise # 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): 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. # 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): def process_response(self, request, resp):
if settings.DEBUG and resp.status_code >= 400: if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error # 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 # https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"' 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 self._needs_csp(request, resp):
if 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return resp
if not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp)) resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
elif 'Content-Security-Policy' in resp: elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy'] del resp['Content-Security-Policy']
return resp 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): def _build_csp(self, request, resp):
url = resolve(request.path_info) url = resolve(request.path_info)
@@ -387,13 +417,6 @@ class SecurityMiddleware(MiddlewareMixin):
h['style-src'] += ["'unsafe-inline'"] h['style-src'] += ["'unsafe-inline'"]
h['connect-src'] += ["http://localhost:5173", "ws://localhost:5173"] 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 # Only include pay.google.com for wallet detection purposes on the Payment selection page
if ( if (
url.url_name == "event.order.pay.change" or url.url_name == "event.order.pay.change" or
@@ -406,6 +429,9 @@ class SecurityMiddleware(MiddlewareMixin):
if settings.LOG_CSP: if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"] h['report-uri'] = ["/csp_report/"]
if request._csp_to_merge:
_merge_csp(h, request._csp_to_merge)
if 'Content-Security-Policy' in resp: if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy'])) _merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
+14 -6
View File
@@ -19,12 +19,10 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # 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/>. # <https://www.gnu.org/licenses/>.
# #
import json import json
import logging import logging
import pathlib import pathlib
import re import re
import secrets
from urllib.parse import urljoin from urllib.parse import urljoin
from urllib.request import urlopen from urllib.request import urlopen
@@ -33,6 +31,10 @@ from django import template
from django.conf import settings from django.conf import settings
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from pretix.base.middleware import (
add_to_response_csp_via_request, calculate_csp_hash,
)
register = template.Library() register = template.Library()
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
_MANIFEST = {} _MANIFEST = {}
@@ -234,10 +236,16 @@ def vite_importmap(context):
if not imports: if not imports:
return "" return ""
# Generate a nonce and store it on the request so the CSP middleware can allow it json_string = json.dumps({"imports": imports})
nonce = secrets.token_urlsafe(16)
# 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') request = context.get('request')
if 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'<script type="importmap" nonce="{nonce}">{json.dumps({"imports": imports})}</script>' return f'<script type="importmap">{json_string}</script>'
+3 -9
View File
@@ -35,7 +35,7 @@ from django.utils.translation import ngettext
from django.views import View from django.views import View
from django.views.generic import DetailView, ListView 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.models import OutgoingMail
from pretix.base.services.mail import mail_send_task from pretix.base.services.mail import mail_send_task
from pretix.control.forms.filter import OutgoingMailFilterForm from pretix.control.forms.filter import OutgoingMailFilterForm
@@ -108,18 +108,12 @@ class OutgoingMailDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequir
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs) response = super().dispatch(request, *args, **kwargs)
if 'Content-Security-Policy' in response: add_to_response_csp(response, {
h = _parse_csp(response['Content-Security-Policy'])
else:
h = {}
csps = {
'frame-src': ['data:'], 'frame-src': ['data:'],
# Unfortuantely, we can't avoid unsafe-inline for style here. # Unfortuantely, we can't avoid unsafe-inline for style here.
# See outgoingmail.js for the protection measures we take. # See outgoingmail.js for the protection measures we take.
'style-src': ["'unsafe-inline'"], 'style-src': ["'unsafe-inline'"],
} })
_merge_csp(h, csps)
response['Content-Security-Policy'] = _render_csp(h)
return response return response
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
+3 -13
View File
@@ -32,7 +32,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.forms import SecretKeySettingsField from pretix.base.forms import SecretKeySettingsField
from pretix.base.logentrytypes import EventLogEntryType, log_entry_types 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.settings import settings_hierarkey
from pretix.base.signals import ( from pretix.base.signals import (
register_global_settings, register_payment_providers, 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.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
(url.namespace == "plugins:paypal2" and url.url_name == "pay") (url.namespace == "plugins:paypal2" and url.url_name == "pay")
): ):
if 'Content-Security-Policy' in response: add_to_response_csp(response, {
h = _parse_csp(response['Content-Security-Policy'])
else:
h = {}
csps = {
'script-src': ['https://www.paypal.com', "'nonce-{}'".format(_nonce(request))], '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 # 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... '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'], '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 '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 return response
+3 -14
View File
@@ -31,7 +31,7 @@ from django.utils.translation import gettext_lazy as _
from paypalhttp import HttpResponse from paypalhttp import HttpResponse
from pretix.base.forms import SecretKeySettingsField 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.settings import settings_hierarkey
from pretix.base.signals import ( from pretix.base.signals import (
logentry_display, register_global_settings, register_payment_providers, 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.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
(url.namespace == "plugins:stripe" and url.url_name in ["sca", "sca.return"]) (url.namespace == "plugins:stripe" and url.url_name in ["sca", "sca.return"])
): ):
if 'Content-Security-Policy' in response: add_to_response_csp(response, {
h = _parse_csp(response['Content-Security-Policy'])
else:
h = {}
# https://stripe.com/docs/security/guide#content-security-policy
csps = {
'connect-src': ['https://api.stripe.com'], 'connect-src': ['https://api.stripe.com'],
'frame-src': ['https://js.stripe.com', 'https://hooks.stripe.com'], 'frame-src': ['https://js.stripe.com', 'https://hooks.stripe.com'],
'script-src': ['https://js.stripe.com'], 'script-src': ['https://js.stripe.com'],
} })
_merge_csp(h, csps)
if h:
response['Content-Security-Policy'] = _render_csp(h)
return response return response