mirror of
https://github.com/pretix/pretix.git
synced 2026-08-24 13:02:00 +00:00
Improve CSP handling in vite integration; refactor CSP handling into helper functions (Z#23240534) (#6387)
This commit is contained in:
@@ -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']))
|
||||||
|
|
||||||
|
|||||||
@@ -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>'
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user