mirror of
https://github.com/pretix/pretix.git
synced 2026-07-31 09:15:08 +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
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
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']))
|
||||
|
||||
|
||||
@@ -19,12 +19,10 @@
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
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'<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.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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user